891 lines
26 KiB
TypeScript
891 lines
26 KiB
TypeScript
import React, { useState } from "react";
|
|
import {
|
|
WizardTop,
|
|
WizardBody,
|
|
WizardQ,
|
|
WizardFooter,
|
|
LANE_LABELS,
|
|
PresetGroup,
|
|
Field,
|
|
} from "./onboarding-primitives";
|
|
|
|
const ENTREP_TOTAL = 5;
|
|
const ENTREP_STEP_NAMES = ["Type", "Idea", "Status", "Details", "Name"];
|
|
|
|
const IDEA_PROMPTS = [
|
|
"A community for indie game devs to swap playtesters, with weekly demo nights",
|
|
"An AI tool that turns my handwritten recipe notes into a clean cookbook for my family",
|
|
"A waitlist + scheduler for my pottery studio — small classes, six people max",
|
|
"A subscription box service for cold-brew enthusiasts, with monthly tasting cards",
|
|
"A simple tool that turns my Strava data into framed art prints I can sell",
|
|
];
|
|
|
|
export function EntrepIdea({ value, onChange }) {
|
|
const [phIdx, setPhIdx] = useState(0);
|
|
const [phChars, setPhChars] = useState(0);
|
|
const [deleting, setDeleting] = useState(false);
|
|
|
|
React.useEffect(() => {
|
|
if (value.length > 0) return undefined;
|
|
const full = IDEA_PROMPTS[phIdx];
|
|
const speed = deleting ? 18 : 38;
|
|
const t = setTimeout(() => {
|
|
if (!deleting) {
|
|
if (phChars < full.length) setPhChars(phChars + 1);
|
|
else setTimeout(() => setDeleting(true), 1500);
|
|
} else {
|
|
if (phChars > 0) setPhChars(phChars - 1);
|
|
else {
|
|
setDeleting(false);
|
|
setPhIdx((phIdx + 1) % IDEA_PROMPTS.length);
|
|
}
|
|
}
|
|
}, speed);
|
|
return () => clearTimeout(t);
|
|
}, [value, phIdx, phChars, deleting]);
|
|
|
|
return (
|
|
<>
|
|
<WizardQ
|
|
title="What are you building?"
|
|
sub="Don't worry if it's not crisp yet — just dump your thoughts. Talk like you would to a friend."
|
|
/>
|
|
<div style={{ position: "relative" }}>
|
|
<textarea
|
|
className="wiz-input"
|
|
style={{ minHeight: 200, fontSize: 15 }}
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
autoFocus
|
|
aria-label="Describe your idea"
|
|
/>
|
|
{value.length === 0 && (
|
|
<div
|
|
style={{
|
|
position: "absolute",
|
|
top: 12,
|
|
left: 14,
|
|
right: 14,
|
|
pointerEvents: "none",
|
|
color: "var(--fg-faint)",
|
|
font: "14.5px/1.5 var(--font-sans)",
|
|
}}
|
|
>
|
|
{IDEA_PROMPTS[phIdx].slice(0, phChars)}
|
|
<span
|
|
style={{
|
|
display: "inline-block",
|
|
width: 7,
|
|
height: 14,
|
|
verticalAlign: "-2px",
|
|
background: "var(--accent)",
|
|
marginLeft: 1,
|
|
animation: "blink 1s steps(2) infinite",
|
|
boxShadow: "0 0 10px var(--accent-glow)",
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div
|
|
className="mono"
|
|
style={{
|
|
fontSize: 11,
|
|
color: "var(--fg-faint)",
|
|
letterSpacing: "0.06em",
|
|
marginTop: -16,
|
|
}}
|
|
>
|
|
{value.length} chars · be specific where it matters
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const ARCHETYPES = [
|
|
{
|
|
id: "saas",
|
|
label: "Web App / SaaS",
|
|
desc: "Dashboards, tools, interactive portals",
|
|
},
|
|
{
|
|
id: "marketplace",
|
|
label: "Marketplace",
|
|
desc: "Directories, bookings, listings",
|
|
},
|
|
{
|
|
id: "marketing",
|
|
label: "Marketing Site",
|
|
desc: "Portfolios, lead capture, landing pages",
|
|
},
|
|
{
|
|
id: "ecommerce",
|
|
label: "Online Store",
|
|
desc: "Carts, checkouts, selling physical/digital goods",
|
|
},
|
|
{
|
|
id: "mobile",
|
|
label: "Mobile App",
|
|
desc: "iOS and Android mobile applications",
|
|
},
|
|
{
|
|
id: "blog",
|
|
label: "Blog / Publication",
|
|
desc: "Newsletters, articles, content hubs",
|
|
},
|
|
{
|
|
id: "not_sure",
|
|
label: "I'm not sure",
|
|
desc: "Let Vibn help you decide based on your description",
|
|
fullWidth: true,
|
|
},
|
|
];
|
|
|
|
function EntrepType({ value, onChange }) {
|
|
return (
|
|
<>
|
|
<WizardQ
|
|
title="What kind of product is it?"
|
|
sub="Helps Vibn set up the right database, integrations, and starting code."
|
|
/>
|
|
<PresetGroup
|
|
options={ARCHETYPES.map((a) => ({
|
|
id: a.id,
|
|
label: a.label,
|
|
desc: a.desc,
|
|
icon: undefined,
|
|
fullWidth: a.fullWidth,
|
|
}))}
|
|
value={value}
|
|
onChange={onChange}
|
|
columns={2}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
const STATUS_OPTIONS = [
|
|
{
|
|
id: "scratch",
|
|
label: "We are starting from scratch",
|
|
desc: "No code, no wireframes, no assets. Just a clean slate and a fresh vision.",
|
|
},
|
|
{
|
|
id: "research",
|
|
label: "I've done some research I can share",
|
|
desc: "I've had some chats with AI, or made some documents I can share to get started.",
|
|
},
|
|
{
|
|
id: "existing",
|
|
label: "I've already built some of the project",
|
|
desc: "I can share some code, designs or connect a repo",
|
|
},
|
|
];
|
|
|
|
function EntrepStatus({ value, onChange }) {
|
|
return (
|
|
<>
|
|
<WizardQ
|
|
title="Where are you starting from?"
|
|
sub="Helps Vibn set up the right repository, data pipelines, and workspace scope."
|
|
/>
|
|
<PresetGroup
|
|
options={STATUS_OPTIONS.map((o) => ({
|
|
id: o.id,
|
|
label: o.label,
|
|
desc: o.desc,
|
|
icon: undefined,
|
|
}))}
|
|
value={value}
|
|
onChange={onChange}
|
|
columns={1}
|
|
minimal={false}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ── Step 3 DYNAMIC: Research Ingestion ──────────────────────────────────────
|
|
function EntrepResearch({ files = [], brief = "", onChange }) {
|
|
const [uploading, setUploading] = useState<string | null>(null);
|
|
const [progress, setProgress] = useState(0);
|
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const selectedFiles = Array.from(e.target.files ?? []);
|
|
if (selectedFiles.length === 0) return;
|
|
|
|
const label =
|
|
selectedFiles.length === 1
|
|
? selectedFiles[0].name
|
|
: `${selectedFiles.length} files`;
|
|
setUploading(label);
|
|
setProgress(0);
|
|
|
|
// Simulate a smooth upload progress bar
|
|
const interval = setInterval(() => {
|
|
setProgress((p) => {
|
|
if (p >= 100) {
|
|
clearInterval(interval);
|
|
setTimeout(() => {
|
|
const newFiles = selectedFiles.map((f) => ({
|
|
name: f.name,
|
|
size: `${(f.size / 1024).toFixed(1)} KB`,
|
|
}));
|
|
onChange({
|
|
files: [...files, ...newFiles],
|
|
});
|
|
setUploading(null);
|
|
}, 300);
|
|
return 100;
|
|
}
|
|
return p + 15;
|
|
});
|
|
}, 80);
|
|
};
|
|
|
|
const removeFile = (name: string) => {
|
|
onChange({
|
|
files: files.filter(
|
|
(f: { name: string; size: string }) => f.name !== name,
|
|
),
|
|
});
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<WizardQ
|
|
title="Share your research"
|
|
sub="Vibn parses and ingests these directly into your project's knowledge base."
|
|
/>
|
|
|
|
{/* File Uploader */}
|
|
<Field
|
|
label="Upload research documents"
|
|
optional
|
|
hint="PDFs, briefs, wireframes, or text notes."
|
|
>
|
|
<div style={{ position: "relative" }}>
|
|
<label
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
minHeight: 104,
|
|
border: "1px dashed var(--hairline-2)",
|
|
borderRadius: 12,
|
|
background: "oklch(0.16 0.008 60 / 0.4)",
|
|
cursor: "pointer",
|
|
padding: 16,
|
|
textAlign: "center",
|
|
transition: "border-color .15s",
|
|
}}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.borderColor = "var(--accent)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.borderColor = "var(--hairline-2)";
|
|
}}
|
|
>
|
|
<input
|
|
type="file"
|
|
onChange={handleFileChange}
|
|
multiple
|
|
style={{ display: "none" }}
|
|
/>
|
|
{uploading ? (
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
maxWidth: 200,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 8,
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<span className="spinner" style={{ width: 20, height: 20 }} />
|
|
<div style={{ fontSize: 13, color: "var(--fg)" }}>
|
|
Uploading {uploading}…
|
|
</div>
|
|
<div
|
|
style={{
|
|
width: "100%",
|
|
height: 3,
|
|
background: "rgba(255,255,255,0.08)",
|
|
borderRadius: 1.5,
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: `${progress}%`,
|
|
height: "100%",
|
|
background: "var(--accent)",
|
|
transition: "width .1s",
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<svg
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="var(--fg-faint)"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
style={{ marginBottom: 8 }}
|
|
>
|
|
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" />
|
|
</svg>
|
|
<div
|
|
style={{
|
|
fontSize: 13.5,
|
|
fontWeight: 500,
|
|
color: "var(--fg-dim)",
|
|
marginBottom: 2,
|
|
}}
|
|
>
|
|
Drag & drop files or{" "}
|
|
<span
|
|
style={{
|
|
color: "var(--accent)",
|
|
textDecoration: "underline",
|
|
}}
|
|
>
|
|
browse
|
|
</span>
|
|
</div>
|
|
<div
|
|
style={{
|
|
fontSize: 11,
|
|
color: "var(--fg-faint)",
|
|
fontFamily: "var(--font-mono)",
|
|
}}
|
|
>
|
|
Maximum size: 20MB per file
|
|
</div>
|
|
</>
|
|
)}
|
|
</label>
|
|
</div>
|
|
</Field>
|
|
|
|
{/* Uploaded File List */}
|
|
{files.length > 0 && (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 6,
|
|
marginBottom: 12,
|
|
}}
|
|
>
|
|
{files.map((file: { name: string; size: string }) => (
|
|
<div
|
|
key={file.name}
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
padding: "8px 12px",
|
|
background: "oklch(0.18 0.009 60 / 0.5)",
|
|
border: "1px solid var(--hairline)",
|
|
borderRadius: 10,
|
|
}}
|
|
>
|
|
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
|
<svg
|
|
width="14"
|
|
height="14"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="var(--accent)"
|
|
strokeWidth="1.8"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
<path d="M14 2v6h6" />
|
|
<path d="M16 13H8M16 17H8M10 9H8" />
|
|
</svg>
|
|
<span
|
|
style={{ fontSize: 13, fontWeight: 500, color: "var(--fg)" }}
|
|
>
|
|
{file.name}
|
|
</span>
|
|
<span
|
|
style={{
|
|
fontSize: 11,
|
|
color: "var(--fg-faint)",
|
|
fontFamily: "var(--font-mono)",
|
|
}}
|
|
>
|
|
({file.size})
|
|
</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => removeFile(file.name)}
|
|
style={{ color: "var(--fg-faint)", cursor: "pointer" }}
|
|
onMouseEnter={(e) => {
|
|
e.currentTarget.style.color = "oklch(0.65 0.18 25)";
|
|
}}
|
|
onMouseLeave={(e) => {
|
|
e.currentTarget.style.color = "var(--fg-faint)";
|
|
}}
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Conversation Paste area */}
|
|
<Field
|
|
label="Or paste an AI conversation / notes"
|
|
optional
|
|
hint="Paste previous chats or text notes to help guide the AI."
|
|
>
|
|
<textarea
|
|
className="wiz-input"
|
|
style={{ minHeight: 120, fontSize: 14.5 }}
|
|
placeholder="Paste previous ChatGPT/Claude conversations or general notes here…"
|
|
value={brief}
|
|
onChange={(e) => onChange({ brief: e.target.value })}
|
|
/>
|
|
</Field>
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ── Step 3 DYNAMIC: Existing Project Ingestion ──────────────────────────────
|
|
function EntrepExisting({ checklist = {}, onChange }) {
|
|
const [uploading, setUploading] = useState<"designs" | "code" | null>(null);
|
|
const [progress, setProgress] = useState(0);
|
|
|
|
const toggleCheck = (key: string) => {
|
|
onChange({ checklist: { ...checklist, [key]: !checklist[key] } });
|
|
};
|
|
|
|
const handleFileChange = (
|
|
key: "designs" | "code",
|
|
e: React.ChangeEvent<HTMLInputElement>,
|
|
) => {
|
|
const selectedFiles = Array.from(e.target.files ?? []);
|
|
if (selectedFiles.length === 0) return;
|
|
|
|
setUploading(key);
|
|
setProgress(0);
|
|
|
|
const interval = setInterval(() => {
|
|
setProgress((p) => {
|
|
if (p >= 100) {
|
|
clearInterval(interval);
|
|
setTimeout(() => {
|
|
const currentFiles = checklist[`${key}Files`] || [];
|
|
const newFiles = selectedFiles.map((f) => ({
|
|
name: f.name,
|
|
size: `${(f.size / 1024).toFixed(1)} KB`,
|
|
}));
|
|
onChange({
|
|
checklist: {
|
|
...checklist,
|
|
[`${key}Files`]: [...currentFiles, ...newFiles],
|
|
},
|
|
});
|
|
setUploading(null);
|
|
}, 300);
|
|
return 100;
|
|
}
|
|
return p + 15;
|
|
});
|
|
}, 80);
|
|
};
|
|
|
|
const removeFile = (key: "designs" | "code", name: string) => {
|
|
const currentFiles = checklist[`${key}Files`] || [];
|
|
onChange({
|
|
checklist: {
|
|
...checklist,
|
|
[`${key}Files`]: currentFiles.filter(
|
|
(f: { name: string; size: string }) => f.name !== name,
|
|
),
|
|
},
|
|
});
|
|
};
|
|
|
|
const renderUploader = (key: "designs" | "code", label: string) => {
|
|
const files = checklist[`${key}Files`] || [];
|
|
return (
|
|
<div
|
|
style={{
|
|
marginTop: 10,
|
|
paddingLeft: 22,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
gap: 6,
|
|
}}
|
|
>
|
|
<label
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
height: 48,
|
|
border: "1px dashed var(--hairline-2)",
|
|
borderRadius: 8,
|
|
background: "oklch(0.16 0.008 60 / 0.2)",
|
|
cursor: "pointer",
|
|
textAlign: "center",
|
|
width: "100%",
|
|
}}
|
|
>
|
|
<input
|
|
type="file"
|
|
onChange={(e) => handleFileChange(key, e)}
|
|
multiple
|
|
style={{ display: "none" }}
|
|
/>
|
|
{uploading === key ? (
|
|
<div style={{ display: "flex", gap: 8, alignItems: "center" }}>
|
|
<span className="spinner" style={{ width: 12, height: 12 }} />
|
|
<span style={{ fontSize: 12, color: "var(--fg-dim)" }}>
|
|
Uploading ({progress}%)…
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<span style={{ fontSize: 12.5, color: "var(--accent)" }}>
|
|
+ Upload {label}
|
|
</span>
|
|
)}
|
|
</label>
|
|
|
|
{files.length > 0 && (
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
|
|
{files.map((file: { name: string; size: string }) => (
|
|
<div
|
|
key={file.name}
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
padding: "6px 10px",
|
|
background: "oklch(0.18 0.009 60 / 0.3)",
|
|
border: "1px solid var(--hairline)",
|
|
borderRadius: 8,
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
fontSize: 12,
|
|
fontWeight: 500,
|
|
color: "var(--fg-dim)",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
whiteSpace: "nowrap",
|
|
}}
|
|
>
|
|
{file.name} ({file.size})
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => removeFile(key, file.name)}
|
|
style={{
|
|
color: "var(--fg-faint)",
|
|
cursor: "pointer",
|
|
fontSize: 10,
|
|
}}
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const list = [
|
|
{ key: "prd", label: "Product requirements doc (PRD) or similar" },
|
|
{
|
|
key: "designs",
|
|
label: "Created designs we can upload",
|
|
uploadLabel: "design files (PDF/Figma/Sketch)",
|
|
},
|
|
{
|
|
key: "code",
|
|
label: "Generated some code we can upload",
|
|
uploadLabel: "code files (zip/JSON/JS)",
|
|
},
|
|
{ key: "github", label: "Have a repo in GitHub we can connect" },
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<WizardQ
|
|
title="Check what you've done"
|
|
sub="Help us align. Let Vibn know what assets you have ready to connect."
|
|
/>
|
|
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
|
|
{list.map((item) => {
|
|
const active = !!checklist[item.key];
|
|
return (
|
|
<div
|
|
key={item.key}
|
|
style={{ display: "flex", flexDirection: "column" }}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleCheck(item.key)}
|
|
style={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 12,
|
|
textAlign: "left",
|
|
padding: "14px 16px",
|
|
borderRadius: 12,
|
|
border: `1px solid ${active ? "var(--accent)" : "var(--hairline)"}`,
|
|
background: active
|
|
? "oklch(0.20 0.04 35 / 0.4)"
|
|
: "oklch(0.18 0.009 60 / 0.6)",
|
|
boxShadow: active
|
|
? "0 0 0 3px oklch(0.74 0.175 35 / 0.1)"
|
|
: "none",
|
|
transition: "border-color .15s, background .15s",
|
|
color: "var(--fg)",
|
|
cursor: "pointer",
|
|
}}
|
|
>
|
|
<span
|
|
style={{
|
|
width: 14,
|
|
height: 14,
|
|
borderRadius: 3,
|
|
border: `1.5px solid ${active ? "var(--accent)" : "var(--hairline-2)"}`,
|
|
background: active ? "var(--accent)" : "transparent",
|
|
display: "grid",
|
|
placeItems: "center",
|
|
color: "var(--accent-fg)",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{active && (
|
|
<svg
|
|
width="8"
|
|
height="8"
|
|
viewBox="0 0 16 16"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="3"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
aria-hidden="true"
|
|
>
|
|
<path d="m3 8.5 3.2 3.2L13 5" />
|
|
</svg>
|
|
)}
|
|
</span>
|
|
<span style={{ fontSize: 14, fontWeight: 500 }}>
|
|
{item.label}
|
|
</span>
|
|
</button>
|
|
|
|
{active &&
|
|
item.uploadLabel &&
|
|
renderUploader(
|
|
item.key as "designs" | "code",
|
|
item.uploadLabel,
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function EntrepName({ value, onChange, ideaDescription }) {
|
|
// Pre-fill the workspace name from what they typed, or fall back
|
|
const trimmed = String(value || "").trim();
|
|
const slug = trimmed
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
.slice(0, 32);
|
|
|
|
return (
|
|
<>
|
|
<WizardQ
|
|
title="Name your workspace"
|
|
sub="This is your home base. You can rename it later."
|
|
/>
|
|
<Field label="Workspace name">
|
|
<input
|
|
type="text"
|
|
className="wiz-input"
|
|
placeholder="My Workspace"
|
|
value={value}
|
|
autoFocus
|
|
maxLength={48}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
/>
|
|
</Field>
|
|
{slug && (
|
|
<div
|
|
className="mono"
|
|
style={{ marginTop: 4, fontSize: 12.5, color: "var(--fg-mute)" }}
|
|
>
|
|
vibnai.com/<span style={{ color: "var(--accent)" }}>{slug}</span>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
// ── Path wrapper ───────────────────────────────────────────────────────────
|
|
export function EntrepreneurPath({
|
|
data,
|
|
onUpdate,
|
|
onBack,
|
|
onClose,
|
|
onComplete,
|
|
onJumpToStep,
|
|
step,
|
|
}) {
|
|
const next = () => {
|
|
if (step === 2 && data.projectStatus === "scratch") {
|
|
// Bypasses step 3 and goes straight to step 4 (Name Workspace)
|
|
onJumpToStep(4);
|
|
} else if (step < ENTREP_TOTAL - 1) {
|
|
onJumpToStep(step + 1);
|
|
} else {
|
|
onComplete();
|
|
}
|
|
};
|
|
const back = () => {
|
|
if (step === 0) {
|
|
onBack();
|
|
} else if (step === 4 && data.projectStatus === "scratch") {
|
|
// Bypasses step 3 and goes straight back to step 2 (Status selection)
|
|
onJumpToStep(2);
|
|
} else {
|
|
onJumpToStep(step - 1);
|
|
}
|
|
};
|
|
|
|
let body, canNext;
|
|
const onSkip = null;
|
|
if (step === 0) {
|
|
body = (
|
|
<EntrepType
|
|
value={data.productType || ""}
|
|
onChange={(v) => onUpdate({ productType: v })}
|
|
/>
|
|
);
|
|
canNext = !!data.productType;
|
|
} else if (step === 1) {
|
|
body = (
|
|
<EntrepIdea
|
|
value={data.idea || ""}
|
|
onChange={(v) => {
|
|
const words = v
|
|
.trim()
|
|
.split(/\s+/)
|
|
.slice(0, 4)
|
|
.join(" ")
|
|
.replace(/[^a-zA-Z0-9\s]/g, "");
|
|
const cleanName = words
|
|
? words.charAt(0).toUpperCase() + words.slice(1)
|
|
: "";
|
|
onUpdate({
|
|
idea: v,
|
|
...(!data.bizName ? { bizName: cleanName } : {}),
|
|
});
|
|
}}
|
|
/>
|
|
);
|
|
canNext = (data.idea || "").trim().length >= 8;
|
|
} else if (step === 2) {
|
|
body = (
|
|
<EntrepStatus
|
|
value={data.projectStatus || ""}
|
|
onChange={(v) => onUpdate({ projectStatus: v })}
|
|
/>
|
|
);
|
|
canNext = !!data.projectStatus;
|
|
} else if (step === 3) {
|
|
// Dynamic Step 3 details page based on Page 4 selection
|
|
if (data.projectStatus === "research") {
|
|
body = (
|
|
<EntrepResearch
|
|
files={data.researchFiles || []}
|
|
brief={data.researchBrief || ""}
|
|
onChange={(patch) =>
|
|
onUpdate({
|
|
researchFiles: patch.files ?? data.researchFiles,
|
|
researchBrief: patch.brief ?? data.researchBrief,
|
|
})
|
|
}
|
|
/>
|
|
);
|
|
canNext = true; // optional research
|
|
} else {
|
|
body = (
|
|
<EntrepExisting
|
|
checklist={data.existingChecklist || {}}
|
|
onChange={(patch) =>
|
|
onUpdate({
|
|
existingChecklist: patch.checklist ?? data.existingChecklist,
|
|
})
|
|
}
|
|
/>
|
|
);
|
|
// Let them continue if they checked at least one thing, or they can skip.
|
|
canNext = true;
|
|
}
|
|
} else {
|
|
body = (
|
|
<EntrepName
|
|
value={data.bizName || ""}
|
|
onChange={(v) => onUpdate({ bizName: v })}
|
|
ideaDescription={data.idea || ""}
|
|
/>
|
|
);
|
|
canNext = (data.bizName || "").trim().length > 0;
|
|
}
|
|
|
|
// 5 total: fork(1) + 4 path steps
|
|
return (
|
|
<>
|
|
<WizardTop
|
|
onBack={back}
|
|
onClose={onClose}
|
|
lane={LANE_LABELS.entrepreneur}
|
|
stepText={ENTREP_STEP_NAMES[step]}
|
|
current={step + 2}
|
|
total={5}
|
|
/>
|
|
<WizardBody
|
|
width={step === 0 || step === 2 || step === 3 ? "wide" : null}
|
|
>
|
|
{body}
|
|
<WizardFooter
|
|
onNext={next}
|
|
canNext={canNext}
|
|
nextLabel={step === ENTREP_TOTAL - 1 ? "Build →" : "Continue"}
|
|
hint={canNext ? "⌘↵" : null}
|
|
onSkip={onSkip}
|
|
skipLabel="Pick for me"
|
|
/>
|
|
</WizardBody>
|
|
</>
|
|
);
|
|
}
|