nextjs-hackathon-stack 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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -0
  3. package/dist/index.js +152 -0
  4. package/package.json +54 -0
  5. package/template/.cursor/agents/business-intelligence.md +38 -0
  6. package/template/.cursor/agents/code-reviewer.md +74 -0
  7. package/template/.cursor/agents/frontend.md +45 -0
  8. package/template/.cursor/agents/security-researcher.md +49 -0
  9. package/template/.cursor/agents/technical-lead.md +36 -0
  10. package/template/.cursor/agents/test-qa.md +85 -0
  11. package/template/.cursor/rules/ai.mdc +53 -0
  12. package/template/.cursor/rules/architecture.mdc +42 -0
  13. package/template/.cursor/rules/coding-standards.mdc +53 -0
  14. package/template/.cursor/rules/components.mdc +33 -0
  15. package/template/.cursor/rules/data-fetching.mdc +63 -0
  16. package/template/.cursor/rules/forms.mdc +59 -0
  17. package/template/.cursor/rules/general.mdc +52 -0
  18. package/template/.cursor/rules/nextjs.mdc +46 -0
  19. package/template/.cursor/rules/security.mdc +54 -0
  20. package/template/.cursor/rules/supabase.mdc +36 -0
  21. package/template/.cursor/rules/testing.mdc +116 -0
  22. package/template/.cursor/skills/create-api-route/SKILL.md +62 -0
  23. package/template/.cursor/skills/create-feature/SKILL.md +52 -0
  24. package/template/.cursor/skills/review-branch/SKILL.md +61 -0
  25. package/template/.cursor/skills/security-audit/SKILL.md +69 -0
  26. package/template/.env.example +24 -0
  27. package/template/.requirements/README.md +15 -0
  28. package/template/.requirements/auth.md +50 -0
  29. package/template/.requirements/template.md +25 -0
  30. package/template/README.md +98 -0
  31. package/template/components.json +19 -0
  32. package/template/drizzle.config.ts +10 -0
  33. package/template/eslint.config.ts +66 -0
  34. package/template/middleware.ts +10 -0
  35. package/template/next.config.ts +31 -0
  36. package/template/package.json.tmpl +62 -0
  37. package/template/playwright.config.ts +22 -0
  38. package/template/src/app/(auth)/login/page.tsx +12 -0
  39. package/template/src/app/(protected)/layout.tsx +19 -0
  40. package/template/src/app/(protected)/page.tsx +16 -0
  41. package/template/src/app/api/auth/callback/route.ts +21 -0
  42. package/template/src/app/globals.css +1 -0
  43. package/template/src/app/layout.tsx +21 -0
  44. package/template/src/e2e/chat.spec.ts +8 -0
  45. package/template/src/e2e/home.spec.ts +8 -0
  46. package/template/src/e2e/login.spec.ts +21 -0
  47. package/template/src/features/auth/__tests__/login-form.test.tsx +43 -0
  48. package/template/src/features/auth/__tests__/use-auth.test.ts +46 -0
  49. package/template/src/features/auth/actions/login.action.ts +38 -0
  50. package/template/src/features/auth/actions/logout.action.ts +10 -0
  51. package/template/src/features/auth/components/login-form.tsx +80 -0
  52. package/template/src/features/auth/hooks/use-auth.ts +17 -0
  53. package/template/src/features/chat/__tests__/chat-ui.test.tsx +30 -0
  54. package/template/src/features/chat/__tests__/route.test.ts +19 -0
  55. package/template/src/features/chat/api/route.ts +16 -0
  56. package/template/src/features/chat/components/chat-ui.tsx +47 -0
  57. package/template/src/features/chat/hooks/use-chat.ts +1 -0
  58. package/template/src/features/tts/__tests__/route.test.ts +13 -0
  59. package/template/src/features/tts/__tests__/tts-player.test.tsx +20 -0
  60. package/template/src/features/tts/api/route.ts +14 -0
  61. package/template/src/features/tts/components/tts-player.tsx +59 -0
  62. package/template/src/features/video/__tests__/route.test.ts +13 -0
  63. package/template/src/features/video/__tests__/video-generator.test.tsx +20 -0
  64. package/template/src/features/video/api/route.ts +14 -0
  65. package/template/src/features/video/components/video-generator.tsx +56 -0
  66. package/template/src/shared/__tests__/ai.test.ts +8 -0
  67. package/template/src/shared/__tests__/minimax-media.test.ts +57 -0
  68. package/template/src/shared/__tests__/providers.test.tsx +14 -0
  69. package/template/src/shared/__tests__/schema.test.ts +18 -0
  70. package/template/src/shared/__tests__/supabase-client.test.ts +13 -0
  71. package/template/src/shared/__tests__/supabase-server.test.ts +22 -0
  72. package/template/src/shared/components/providers.tsx +19 -0
  73. package/template/src/shared/db/index.ts +7 -0
  74. package/template/src/shared/db/schema.ts +15 -0
  75. package/template/src/shared/lib/ai.ts +8 -0
  76. package/template/src/shared/lib/minimax-media.ts +63 -0
  77. package/template/src/shared/lib/supabase/client.ts +8 -0
  78. package/template/src/shared/lib/supabase/middleware.ts +40 -0
  79. package/template/src/shared/lib/supabase/server.ts +26 -0
  80. package/template/src/test-setup.ts +1 -0
  81. package/template/tailwind.css +20 -0
  82. package/template/tsconfig.json +31 -0
  83. package/template/vitest.config.ts +33 -0
@@ -0,0 +1,8 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { aiModel } from "../lib/ai";
3
+
4
+ describe("aiModel", () => {
5
+ it("exports aiModel", () => {
6
+ expect(aiModel).toBeDefined();
7
+ });
8
+ });
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { generateVideo, synthesizeSpeech } from "../lib/minimax-media";
3
+
4
+ const mockFetch = vi.fn();
5
+ global.fetch = mockFetch;
6
+
7
+ describe("generateVideo", () => {
8
+ beforeEach(() => {
9
+ vi.clearAllMocks();
10
+ });
11
+
12
+ it("returns task_id when API responds successfully", async () => {
13
+ // Arrange
14
+ mockFetch.mockResolvedValue({
15
+ ok: true,
16
+ json: async () => ({ task_id: "abc123" }),
17
+ });
18
+
19
+ // Act
20
+ const taskId = await generateVideo("A cat on the moon");
21
+
22
+ // Assert
23
+ expect(taskId).toBe("abc123");
24
+ });
25
+
26
+ it("throws when API returns non-ok response", async () => {
27
+ // Arrange
28
+ mockFetch.mockResolvedValue({ ok: false, statusText: "Bad Request" });
29
+
30
+ // Act + Assert
31
+ await expect(generateVideo("test")).rejects.toThrow("Video generation failed");
32
+ });
33
+ });
34
+
35
+ describe("synthesizeSpeech", () => {
36
+ it("returns audio_file URL when API responds successfully", async () => {
37
+ // Arrange
38
+ mockFetch.mockResolvedValue({
39
+ ok: true,
40
+ json: async () => ({ audio_file: "https://example.com/audio.mp3" }),
41
+ });
42
+
43
+ // Act
44
+ const url = await synthesizeSpeech("Hello world");
45
+
46
+ // Assert
47
+ expect(url).toBe("https://example.com/audio.mp3");
48
+ });
49
+
50
+ it("throws when API returns non-ok response", async () => {
51
+ // Arrange
52
+ mockFetch.mockResolvedValue({ ok: false, statusText: "Server Error" });
53
+
54
+ // Act + Assert
55
+ await expect(synthesizeSpeech("test")).rejects.toThrow("TTS failed");
56
+ });
57
+ });
@@ -0,0 +1,14 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { describe, it, expect } from "vitest";
3
+ import { Providers } from "../components/providers";
4
+
5
+ describe("Providers", () => {
6
+ it("renders children", () => {
7
+ render(
8
+ <Providers>
9
+ <div data-testid="child">Hello</div>
10
+ </Providers>
11
+ );
12
+ expect(screen.getByTestId("child")).toBeInTheDocument();
13
+ });
14
+ });
@@ -0,0 +1,18 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { insertProfileSchema, selectProfileSchema } from "../db/schema";
3
+
4
+ describe("profiles schema", () => {
5
+ it("insertProfileSchema validates required fields", () => {
6
+ const result = insertProfileSchema.safeParse({ id: "uuid", email: "test@example.com" });
7
+ expect(result.success).toBe(true);
8
+ });
9
+
10
+ it("insertProfileSchema rejects invalid email", () => {
11
+ const result = insertProfileSchema.safeParse({ id: "uuid", email: "not-email" });
12
+ expect(result.success).toBe(false);
13
+ });
14
+
15
+ it("selectProfileSchema is defined", () => {
16
+ expect(selectProfileSchema).toBeDefined();
17
+ });
18
+ });
@@ -0,0 +1,13 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+
3
+ vi.mock("@supabase/ssr", () => ({
4
+ createBrowserClient: vi.fn(() => ({ auth: {} })),
5
+ }));
6
+
7
+ describe("supabase client", () => {
8
+ it("createClient returns a supabase client", async () => {
9
+ const { createClient } = await import("../lib/supabase/client");
10
+ const client = createClient();
11
+ expect(client).toBeDefined();
12
+ });
13
+ });
@@ -0,0 +1,22 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+
3
+ vi.mock("@supabase/ssr", () => ({
4
+ createServerClient: vi.fn(() => ({ auth: {} })),
5
+ }));
6
+
7
+ vi.mock("next/headers", () => ({
8
+ cookies: vi.fn(() =>
9
+ Promise.resolve({
10
+ getAll: () => [],
11
+ set: vi.fn(),
12
+ })
13
+ ),
14
+ }));
15
+
16
+ describe("supabase server", () => {
17
+ it("createClient returns a supabase client", async () => {
18
+ const { createClient } = await import("../lib/supabase/server");
19
+ const client = await createClient();
20
+ expect(client).toBeDefined();
21
+ });
22
+ });
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
4
+ import { useState } from "react";
5
+
6
+ export function Providers({ children }: { children: React.ReactNode }) {
7
+ const [queryClient] = useState(
8
+ () =>
9
+ new QueryClient({
10
+ defaultOptions: {
11
+ queries: {
12
+ staleTime: 60 * 1000,
13
+ },
14
+ },
15
+ })
16
+ );
17
+
18
+ return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
19
+ }
@@ -0,0 +1,7 @@
1
+ import { drizzle } from "drizzle-orm/postgres-js";
2
+ import postgres from "postgres";
3
+ import * as schema from "./schema";
4
+
5
+ const client = postgres(process.env["DATABASE_URL"] ?? "");
6
+
7
+ export const db = drizzle(client, { schema });
@@ -0,0 +1,15 @@
1
+ import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
2
+ import { createInsertSchema, createSelectSchema } from "drizzle-zod";
3
+
4
+ export const profiles = pgTable("profiles", {
5
+ id: uuid("id").primaryKey(),
6
+ email: text("email").notNull().unique(),
7
+ createdAt: timestamp("created_at").notNull().defaultNow(),
8
+ updatedAt: timestamp("updated_at").notNull().defaultNow(),
9
+ });
10
+
11
+ export const insertProfileSchema = createInsertSchema(profiles);
12
+ export const selectProfileSchema = createSelectSchema(profiles);
13
+
14
+ export type InsertProfile = typeof profiles.$inferInsert;
15
+ export type SelectProfile = typeof profiles.$inferSelect;
@@ -0,0 +1,8 @@
1
+ import { createOpenAI } from "@ai-sdk/openai";
2
+
3
+ const gateway = createOpenAI({
4
+ baseURL: process.env["AI_GATEWAY_URL"] ?? "https://api.minimaxi.chat/v1",
5
+ apiKey: process.env["MINIMAX_API_KEY"] ?? "",
6
+ });
7
+
8
+ export const aiModel = gateway("minimax/minimax-m2.7");
@@ -0,0 +1,63 @@
1
+ const MINIMAX_API_BASE = "https://api.minimaxi.chat/v1";
2
+
3
+ interface VideoGenerationRequest {
4
+ model: string;
5
+ prompt: string;
6
+ }
7
+
8
+ interface VideoGenerationResponse {
9
+ task_id: string;
10
+ }
11
+
12
+ interface TtsRequest {
13
+ model: string;
14
+ text: string;
15
+ voice_id: string;
16
+ }
17
+
18
+ interface TtsResponse {
19
+ audio_file: string;
20
+ }
21
+
22
+ export async function generateVideo(prompt: string): Promise<string> {
23
+ const response = await fetch(`${MINIMAX_API_BASE}/video_generation`, {
24
+ method: "POST",
25
+ headers: {
26
+ Authorization: `Bearer ${process.env["MINIMAX_API_KEY"] ?? ""}`,
27
+ "Content-Type": "application/json",
28
+ },
29
+ body: JSON.stringify({
30
+ model: "video-01",
31
+ prompt,
32
+ } satisfies VideoGenerationRequest),
33
+ });
34
+
35
+ if (!response.ok) {
36
+ throw new Error(`Video generation failed: ${response.statusText}`);
37
+ }
38
+
39
+ const data = (await response.json()) as VideoGenerationResponse;
40
+ return data.task_id;
41
+ }
42
+
43
+ export async function synthesizeSpeech(text: string, voiceId = "female-shaonv"): Promise<string> {
44
+ const response = await fetch(`${MINIMAX_API_BASE}/t2a_v2`, {
45
+ method: "POST",
46
+ headers: {
47
+ Authorization: `Bearer ${process.env["MINIMAX_API_KEY"] ?? ""}`,
48
+ "Content-Type": "application/json",
49
+ },
50
+ body: JSON.stringify({
51
+ model: "speech-02-hd",
52
+ text,
53
+ voice_id: voiceId,
54
+ } satisfies TtsRequest),
55
+ });
56
+
57
+ if (!response.ok) {
58
+ throw new Error(`TTS failed: ${response.statusText}`);
59
+ }
60
+
61
+ const data = (await response.json()) as TtsResponse;
62
+ return data.audio_file;
63
+ }
@@ -0,0 +1,8 @@
1
+ import { createBrowserClient } from "@supabase/ssr";
2
+
3
+ export function createClient() {
4
+ return createBrowserClient(
5
+ process.env["NEXT_PUBLIC_SUPABASE_URL"]!,
6
+ process.env["NEXT_PUBLIC_SUPABASE_ANON_KEY"]!
7
+ );
8
+ }
@@ -0,0 +1,40 @@
1
+ import { createServerClient } from "@supabase/ssr";
2
+ import { NextResponse, type NextRequest } from "next/server";
3
+
4
+ export async function updateSession(request: NextRequest) {
5
+ let supabaseResponse = NextResponse.next({ request });
6
+
7
+ const supabase = createServerClient(
8
+ process.env["NEXT_PUBLIC_SUPABASE_URL"]!,
9
+ process.env["NEXT_PUBLIC_SUPABASE_ANON_KEY"]!,
10
+ {
11
+ cookies: {
12
+ getAll() {
13
+ return request.cookies.getAll();
14
+ },
15
+ setAll(cookiesToSet) {
16
+ cookiesToSet.forEach(({ name, value }) => request.cookies.set(name, value));
17
+ supabaseResponse = NextResponse.next({ request });
18
+ cookiesToSet.forEach(({ name, value, options }) =>
19
+ supabaseResponse.cookies.set(name, value, options)
20
+ );
21
+ },
22
+ },
23
+ }
24
+ );
25
+
26
+ const {
27
+ data: { user },
28
+ } = await supabase.auth.getUser();
29
+
30
+ const isAuthRoute = request.nextUrl.pathname.startsWith("/login");
31
+ const isProtectedRoute = !isAuthRoute && !request.nextUrl.pathname.startsWith("/api/auth");
32
+
33
+ if (!user && isProtectedRoute && request.nextUrl.pathname !== "/") {
34
+ const redirectUrl = request.nextUrl.clone();
35
+ redirectUrl.pathname = "/login";
36
+ return NextResponse.redirect(redirectUrl);
37
+ }
38
+
39
+ return supabaseResponse;
40
+ }
@@ -0,0 +1,26 @@
1
+ import { createServerClient } from "@supabase/ssr";
2
+ import { cookies } from "next/headers";
3
+
4
+ export async function createClient() {
5
+ const cookieStore = await cookies();
6
+
7
+ return createServerClient(
8
+ process.env["NEXT_PUBLIC_SUPABASE_URL"]!,
9
+ process.env["NEXT_PUBLIC_SUPABASE_ANON_KEY"]!,
10
+ {
11
+ cookies: {
12
+ getAll() {
13
+ return cookieStore.getAll();
14
+ },
15
+ setAll(cookiesToSet) {
16
+ try {
17
+ cookiesToSet.forEach(({ name, value, options }) =>
18
+ cookieStore.set(name, value, options)
19
+ );
20
+ } catch {
21
+ }
22
+ },
23
+ },
24
+ }
25
+ );
26
+ }
@@ -0,0 +1 @@
1
+ import "@testing-library/jest-dom";
@@ -0,0 +1,20 @@
1
+ @import "tailwindcss";
2
+
3
+ @theme {
4
+ --color-background: hsl(0 0% 100%);
5
+ --color-foreground: hsl(222.2 84% 4.9%);
6
+ --color-primary: hsl(222.2 47.4% 11.2%);
7
+ --color-primary-foreground: hsl(210 40% 98%);
8
+ --color-secondary: hsl(210 40% 96.1%);
9
+ --color-secondary-foreground: hsl(222.2 47.4% 11.2%);
10
+ --color-muted: hsl(210 40% 96.1%);
11
+ --color-muted-foreground: hsl(215.4 16.3% 46.9%);
12
+ --color-accent: hsl(210 40% 96.1%);
13
+ --color-accent-foreground: hsl(222.2 47.4% 11.2%);
14
+ --color-destructive: hsl(0 84.2% 60.2%);
15
+ --color-destructive-foreground: hsl(210 40% 98%);
16
+ --color-border: hsl(214.3 31.8% 91.4%);
17
+ --color-input: hsl(214.3 31.8% 91.4%);
18
+ --color-ring: hsl(222.2 84% 4.9%);
19
+ --radius: 0.5rem;
20
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "noImplicitOverride": true,
10
+ "noFallthroughCasesInSwitch": true,
11
+ "exactOptionalPropertyTypes": true,
12
+ "noImplicitReturns": true,
13
+ "noUnusedLocals": true,
14
+ "noUnusedParameters": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "noEmit": true,
17
+ "esModuleInterop": true,
18
+ "module": "esnext",
19
+ "moduleResolution": "bundler",
20
+ "resolveJsonModule": true,
21
+ "isolatedModules": true,
22
+ "jsx": "preserve",
23
+ "incremental": true,
24
+ "plugins": [{ "name": "next" }],
25
+ "paths": {
26
+ "@/*": ["./src/*"]
27
+ }
28
+ },
29
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
30
+ "exclude": ["node_modules"]
31
+ }
@@ -0,0 +1,33 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import react from "@vitejs/plugin-react";
3
+ import { resolve } from "path";
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ test: {
8
+ environment: "jsdom",
9
+ setupFiles: ["./src/test-setup.ts"],
10
+ coverage: {
11
+ provider: "v8",
12
+ thresholds: {
13
+ branches: 100,
14
+ functions: 100,
15
+ lines: 100,
16
+ statements: 100,
17
+ },
18
+ exclude: [
19
+ "node_modules/**",
20
+ "src/e2e/**",
21
+ "src/shared/db/migrations/**",
22
+ "**/*.config.*",
23
+ "**/index.ts",
24
+ "src/app/globals.css",
25
+ ],
26
+ },
27
+ },
28
+ resolve: {
29
+ alias: {
30
+ "@": resolve(__dirname, "./src"),
31
+ },
32
+ },
33
+ });