This repository has been archived on 2026-06-07. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

207 lines
6.4 KiB
TypeScript

"use client";
import { useState, useEffect, type ReactNode } from "react";
import { useRouter } from "next/navigation";
import { toast } from "sonner";
import { JM } from "./modal-theme";
import { DEFAULT_DESIGN_KIT_ID } from "@/lib/design-kits/types";
import {
kitsOrderedForCategory,
type ProductCategoryId,
} from "@/lib/design-kits/product-categories";
import {
SetupHeader, FieldLabel, TextInput, TextArea, AudienceSelector,
ProductCategorySelector,
DesignKitSplitSelector,
SeedDocumentUpload,
PrimaryButton, SecondaryButton, StepDots,
type SetupProps, type Audience,
type SeedDocumentPayload,
} from "./setup-shared";
type BuildSetupProps = SetupProps & {
/** Lets the modal shell widen on step 1 (design kit + preview). */
onWizardStepChange?: (step: number) => void;
};
const DEFAULT_PRODUCT_CATEGORY: ProductCategoryId = "saas-tool";
/**
* "Build your own idea" — three-step setup.
* Step 0: project name + product shape + audience.
* Step 1: design system starter kit (split list + preview).
* Step 2: describe the idea (free text). Becomes the seed message
* for the first AI conversation in the project.
*/
export function BuildSetup({ workspace, onClose, onBack, onWizardStepChange }: BuildSetupProps) {
const router = useRouter();
const [step, setStep] = useState(0);
const [name, setName] = useState("");
const [audience, setAudience] = useState<Audience>("customers");
const [productCategory, setProductCategory] =
useState<ProductCategoryId>(DEFAULT_PRODUCT_CATEGORY);
const [designKitId, setDesignKitId] = useState(DEFAULT_DESIGN_KIT_ID);
const [idea, setIdea] = useState("");
const [seedDocument, setSeedDocument] = useState<SeedDocumentPayload | null>(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
const allowed = kitsOrderedForCategory(productCategory);
const allowedSet = new Set(allowed);
setDesignKitId((prev) => (allowedSet.has(prev) ? prev : allowed[0] ?? prev));
}, [productCategory]);
useEffect(() => {
onWizardStepChange?.(step);
}, [step, onWizardStepChange]);
const totalSteps = 3;
const canContinue0 = name.trim().length > 0;
const canCreate =
idea.trim().length > 4 ||
(seedDocument !== null && idea.trim().length > 0);
const handleBack = () => {
if (step === 0) onBack();
else setStep(step - 1);
};
const handleCreate = async () => {
if (!canCreate) return;
setLoading(true);
try {
const res = await fetch("/api/projects/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
projectName: name.trim(),
projectType: "web-app",
slug: name.toLowerCase().replace(/[^a-z0-9]+/g, "-"),
vision: idea.trim(),
product: { name: name.trim() },
audience,
designKitId,
creationMode: "build",
sourceData: {
idea: idea.trim(),
audience,
designKitId,
productCategory,
},
...(seedDocument ? { seedDocument } : {}),
}),
});
if (!res.ok) {
const err = await res.json();
toast.error(err.error || "Failed to create project");
return;
}
const data = await res.json();
onClose();
router.push(`/${workspace}/project/${data.projectId}/product`);
} catch {
toast.error("Something went wrong");
} finally {
setLoading(false);
}
};
return (
<div style={{ padding: 28 }}>
<SetupHeader
icon="✦" label="Build your own idea" tagline="Start from scratch"
accent="#4338CA" onBack={handleBack} onClose={onClose}
/>
{step === 0 && (
<>
<FieldLabel>Project name</FieldLabel>
<TextInput
value={name}
onChange={setName}
placeholder="e.g. Pet Sitter Pro"
onKeyDown={e => { if (e.key === "Enter" && canContinue0) setStep(1); }}
autoFocus
/>
<ProductCategorySelector value={productCategory} onChange={setProductCategory} />
<AudienceSelector value={audience} onChange={setAudience} />
<FlowFooter step={0} total={totalSteps} primary={
<PrimaryButton onClick={() => setStep(1)} disabled={!canContinue0}>
Next
</PrimaryButton>
} />
</>
)}
{step === 1 && (
<>
<DesignKitSplitSelector
productCategory={productCategory}
value={designKitId}
onChange={setDesignKitId}
/>
<FlowFooter
step={1}
total={totalSteps}
secondary={<SecondaryButton onClick={() => setStep(0)}> Back</SecondaryButton>}
primary={
<PrimaryButton onClick={() => setStep(2)}>
Next
</PrimaryButton>
}
/>
</>
)}
{step === 2 && (
<>
<FieldLabel>What do you want to build?</FieldLabel>
<TextArea
value={idea}
onChange={setIdea}
placeholder="A booking site for my dog grooming business. Customers should be able to book online and pay a deposit by card."
rows={6}
autoFocus
/>
<p style={{ fontSize: 12, color: JM.muted, marginTop: -8, marginBottom: 14, lineHeight: 1.5 }}>
Don&apos;t worry about the tech. Vibn will pick the tools and start building from this description.
</p>
<SeedDocumentUpload value={seedDocument} onChange={setSeedDocument} />
<FlowFooter
step={2}
total={totalSteps}
secondary={<SecondaryButton onClick={() => setStep(1)}> Back</SecondaryButton>}
primary={
<PrimaryButton onClick={handleCreate} disabled={!canCreate} loading={loading}>
Start building
</PrimaryButton>
}
/>
</>
)}
</div>
);
}
function FlowFooter({
step, total, primary, secondary,
}: {
step: number; total: number;
primary: ReactNode; secondary?: ReactNode;
}) {
return (
<div style={{ display: "flex", alignItems: "center", gap: 12, marginTop: 6 }}>
<StepDots step={step} total={total} />
<div style={{ flex: 1 }} />
{secondary}
<div style={{ minWidth: 160 }}>{primary}</div>
</div>
);
}