diff --git a/.gitignore b/.gitignore index d7e1ba2..8f70a2e 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,15 @@ next-env.d.ts # data persistence /data/ + +# Playwright +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + +*storybook.log +storybook-static + +.zed/debug.json diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..2273004 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,23 @@ +import type { StorybookConfig } from "@storybook/nextjs-vite"; + +const config: StorybookConfig = { + "stories": [ + "../stories/**/*.mdx", + "../stories/**/*.stories.@(js|jsx|mjs|ts|tsx)" + ], + "addons": [ + "@chromatic-com/storybook", + "@storybook/addon-docs", + "@storybook/addon-onboarding", + "@storybook/addon-a11y", + "@storybook/addon-vitest" + ], + "framework": { + "name": "@storybook/nextjs-vite", + "options": {} + }, + "staticDirs": [ + "../public" + ] +}; +export default config; \ No newline at end of file diff --git a/.storybook/preview.ts b/.storybook/preview.ts new file mode 100644 index 0000000..eb97b83 --- /dev/null +++ b/.storybook/preview.ts @@ -0,0 +1,21 @@ +import type { Preview } from '@storybook/nextjs-vite' + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + + a11y: { + // 'todo' - show a11y violations in the test UI only + // 'error' - fail CI on a11y violations + // 'off' - skip a11y checks entirely + test: 'todo' + } + }, +}; + +export default preview; \ No newline at end of file diff --git a/.storybook/vitest.setup.ts b/.storybook/vitest.setup.ts new file mode 100644 index 0000000..c5ed05f --- /dev/null +++ b/.storybook/vitest.setup.ts @@ -0,0 +1,7 @@ +import * as a11yAddonAnnotations from "@storybook/addon-a11y/preview"; +import { setProjectAnnotations } from '@storybook/nextjs-vite'; +import * as projectAnnotations from './preview'; + +// This is an important step to apply the right configuration when testing your stories. +// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations +setProjectAnnotations([a11yAddonAnnotations, projectAnnotations]); \ No newline at end of file diff --git a/app/api/projects/create-stream/route.ts b/app/api/projects/create-stream/route.ts index 1bb893d..127bc1c 100644 --- a/app/api/projects/create-stream/route.ts +++ b/app/api/projects/create-stream/route.ts @@ -4,7 +4,10 @@ import { NextRequest } from "next/server"; import { getAuthenticatedUser } from "../../auth/middleware"; import { getUserById } from "../../auth/store"; import { getCodeSandboxService } from "../../services/codesandbox"; -import { validateEnvironment, validateRequiredParams } from "../../utils/responses"; +import { + validateEnvironment, + validateRequiredParams, +} from "../../utils/responses"; interface ProgressStep { id: string; @@ -14,7 +17,7 @@ interface ProgressStep { function createProgressMessage( type: "progress" | "success" | "error", - data: any + data: any, ): string { return `data: ${JSON.stringify({ type, ...data })}\n\n`; } @@ -27,16 +30,19 @@ function createProgressMessage( export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url); const name = searchParams.get("name"); + const version = searchParams.get("version") || "latest"; // Default to latest if not specified if (!name || typeof name !== "string") { return new Response( - createProgressMessage("error", { message: "Project name is required and must be a non-empty string" }), + createProgressMessage("error", { + message: "Project name is required and must be a non-empty string", + }), { status: 400, headers: { "Content-Type": "text/plain", }, - } + }, ); } @@ -58,13 +64,13 @@ export async function GET(request: NextRequest) { const sendProgress = (step: ProgressStep) => { currentStepId = step.status === "in_progress" ? step.id : currentStepId; controller.enqueue( - encoder.encode(createProgressMessage("progress", { step })) + encoder.encode(createProgressMessage("progress", { step })), ); }; const sendSuccess = (projectId: string) => { controller.enqueue( - encoder.encode(createProgressMessage("success", { projectId })) + encoder.encode(createProgressMessage("success", { projectId })), ); controller.close(); }; @@ -80,7 +86,7 @@ export async function GET(request: NextRequest) { }); } controller.enqueue( - encoder.encode(createProgressMessage("error", { message })) + encoder.encode(createProgressMessage("error", { message })), ); controller.close(); }; @@ -107,9 +113,14 @@ export async function GET(request: NextRequest) { // Validate environment variables try { - validateEnvironment(['CSB_API_KEY']); + validateEnvironment(["CSB_API_KEY"]); } catch (error) { - sendError(error instanceof Error ? error.message : "Environment validation failed", "auth"); + sendError( + error instanceof Error + ? error.message + : "Environment validation failed", + "auth", + ); return; } @@ -151,11 +162,14 @@ export async function GET(request: NextRequest) { }); const csbService = getCodeSandboxService(); - const sandbox = await csbService.createSandbox("sdk-example@latest", "private"); + const sandbox = await csbService.createSandbox( + `sdk-example@${version}`, // Use the version parameter here + "private", + ); sendProgress({ id: "sandbox-create", - message: `Sandbox created: ${sandbox.id}`, + message: `Sandbox created: ${sandbox.id} (template: sdk-example@${version})`, status: "completed", }); @@ -191,7 +205,7 @@ export async function GET(request: NextRequest) { `git remote add origin https://github.com/${user.username}/${repo.data.name}.git`, { cwd: "/project/workspace/app", - } + }, ); sendProgress({ @@ -207,17 +221,17 @@ export async function GET(request: NextRequest) { status: "in_progress", }); - await client.commands.run( - [ - "git add .", - `git commit -m "Initial commit"`, - "git branch -M main", - "git push -u origin main", - ], - { - cwd: "/project/workspace/app", - } - ); + // await client.commands.run( + // [ + // "git add .", + // `git commit -m "Initial commit"`, + // "git branch -M main", + // "git push -u origin main", + // ], + // { + // cwd: "/project/workspace/app", + // } + // ); sendProgress({ id: "git-push", @@ -251,7 +265,7 @@ export async function GET(request: NextRequest) { name, sandbox.id, hostToken, - repo.data.html_url + repo.data.html_url, ); sendProgress({ diff --git a/app/components/CreateProjectModal.tsx b/app/components/CreateProjectModal.tsx index aede209..7b2f190 100644 --- a/app/components/CreateProjectModal.tsx +++ b/app/components/CreateProjectModal.tsx @@ -1,7 +1,7 @@ -'use client'; +"use client"; -import { useState } from 'react'; -import { useProjectCreation } from '../hooks/useProjectCreation'; +import { useState } from "react"; +import { useProjectCreation } from "../hooks/useProjectCreation"; interface CreateProjectModalProps { isOpen: boolean; @@ -9,15 +9,29 @@ interface CreateProjectModalProps { onProjectCreated: (projectId: string) => void; } -export default function CreateProjectModal({ isOpen, onClose, onProjectCreated }: CreateProjectModalProps) { - const [projectName, setProjectName] = useState(''); - const { isCreating, progress, error, createProjectWithStream, resetCreation } = useProjectCreation(); +export default function CreateProjectModal({ + isOpen, + onClose, + onProjectCreated, +}: CreateProjectModalProps) { + const [projectName, setProjectName] = useState(""); + const [templateVersion, setTemplateVersion] = useState("latest"); // Add version state + const { + isCreating, + progress, + error, + createProjectWithStream, + resetCreation, + } = useProjectCreation(); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - + try { - const projectId = await createProjectWithStream(projectName); + const projectId = await createProjectWithStream( + projectName, + templateVersion, + ); // Pass version onProjectCreated(projectId); } catch (err) { // Error is handled by the useProjectCreation hook @@ -26,7 +40,7 @@ export default function CreateProjectModal({ isOpen, onClose, onProjectCreated } const handleClose = () => { if (!isCreating) { - setProjectName(''); + setProjectName(""); resetCreation(); onClose(); } @@ -35,35 +49,57 @@ export default function CreateProjectModal({ isOpen, onClose, onProjectCreated } if (!isOpen) return null; return ( -
-
-

Create New Project

- +
+
+

+ Create New Project +

+ {!isCreating && progress.length === 0 ? (
-
-