feat: implement 4 project type flows with unique AI experiences
- New multi-step CreateProjectFlow replaces 2-step modal with TypeSelector and 4 setup components (Fresh Idea, Chat Import, Code Import, Migrate) - overview/page.tsx routes to unique main component per creationMode - FreshIdeaMain: wraps AtlasChat with post-discovery decision banner (Generate PRD vs Plan MVP Test) - ChatImportMain: 3-stage flow (intake → extracting → review) with editable insight buckets (decisions, ideas, questions, architecture, users) - CodeImportMain: 4-stage flow (input → cloning → mapping → surfaces) with architecture map and surface selection - MigrateMain: 5-stage flow with audit, review, planning, and migration plan doc with checkbox-tracked tasks and non-destructive warning banner - New API routes: analyze-chats, analyze-repo, analysis-status, generate-migration-plan (all using Gemini) - ProjectShell: accepts creationMode prop, filters/renames tabs per type (code-import hides PRD, migration hides PRD/Grow/Insights, renames Atlas tab) - Right panel adapts content based on creationMode Made-with: Cursor
This commit is contained in:
84
components/project-creation/ChatImportSetup.tsx
Normal file
84
components/project-creation/ChatImportSetup.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { SetupHeader, FieldLabel, TextInput, PrimaryButton, type SetupProps } from "./setup-shared";
|
||||
|
||||
export function ChatImportSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
const router = useRouter();
|
||||
const [name, setName] = useState("");
|
||||
const [chatText, setChatText] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const canCreate = name.trim().length > 0 && chatText.trim().length > 20;
|
||||
|
||||
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, "-"),
|
||||
product: { name: name.trim() },
|
||||
creationMode: "chat-import",
|
||||
sourceData: { chatText: chatText.trim() },
|
||||
}),
|
||||
});
|
||||
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}/overview`);
|
||||
} catch {
|
||||
toast.error("Something went wrong");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "32px 36px 36px" }}>
|
||||
<SetupHeader
|
||||
icon="⌁" label="Import Chats" tagline="You've been thinking"
|
||||
accent="#2e5a4a" onBack={onBack} onClose={onClose}
|
||||
/>
|
||||
|
||||
<FieldLabel>Project name</FieldLabel>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChange={setName}
|
||||
placeholder="What are you building?"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<FieldLabel>Paste your chat history</FieldLabel>
|
||||
<textarea
|
||||
value={chatText}
|
||||
onChange={e => setChatText(e.target.value)}
|
||||
placeholder={"Paste conversations from ChatGPT, Claude, Gemini, or any AI tool.\n\nAtlas will extract decisions, ideas, open questions, and architecture notes."}
|
||||
rows={8}
|
||||
style={{
|
||||
width: "100%", padding: "12px 14px", marginBottom: 20,
|
||||
borderRadius: 8, border: "1px solid #e0dcd4",
|
||||
background: "#faf8f5", fontSize: "0.85rem", lineHeight: 1.55,
|
||||
fontFamily: "Outfit, sans-serif", color: "#1a1a1a",
|
||||
outline: "none", resize: "vertical", boxSizing: "border-box",
|
||||
}}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
|
||||
/>
|
||||
|
||||
<PrimaryButton onClick={handleCreate} disabled={!canCreate} loading={loading}>
|
||||
Extract & analyse →
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
100
components/project-creation/CodeImportSetup.tsx
Normal file
100
components/project-creation/CodeImportSetup.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { SetupHeader, FieldLabel, TextInput, PrimaryButton, type SetupProps } from "./setup-shared";
|
||||
|
||||
export function CodeImportSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
const router = useRouter();
|
||||
const [name, setName] = useState("");
|
||||
const [repoUrl, setRepoUrl] = useState("");
|
||||
const [pat, setPat] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const isValidUrl = repoUrl.trim().startsWith("http");
|
||||
const canCreate = name.trim().length > 0 && isValidUrl;
|
||||
|
||||
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, "-"),
|
||||
product: { name: name.trim() },
|
||||
creationMode: "code-import",
|
||||
sourceData: { repoUrl: repoUrl.trim(), pat: pat.trim() || undefined },
|
||||
}),
|
||||
});
|
||||
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}/overview`);
|
||||
} catch {
|
||||
toast.error("Something went wrong");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "32px 36px 36px" }}>
|
||||
<SetupHeader
|
||||
icon="⌘" label="Import Code" tagline="Already have a repo"
|
||||
accent="#1a3a5c" onBack={onBack} onClose={onClose}
|
||||
/>
|
||||
|
||||
<FieldLabel>Project name</FieldLabel>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChange={setName}
|
||||
placeholder="What is this project called?"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<FieldLabel>Repository URL</FieldLabel>
|
||||
<TextInput
|
||||
value={repoUrl}
|
||||
onChange={setRepoUrl}
|
||||
placeholder="https://github.com/yourorg/your-repo"
|
||||
/>
|
||||
|
||||
<FieldLabel>
|
||||
Personal Access Token{" "}
|
||||
<span style={{ color: "#b5b0a6", fontWeight: 400 }}>(required for private repos)</span>
|
||||
</FieldLabel>
|
||||
<input
|
||||
type="password"
|
||||
value={pat}
|
||||
onChange={e => setPat(e.target.value)}
|
||||
placeholder="ghp_… or similar"
|
||||
style={{
|
||||
width: "100%", padding: "11px 14px", marginBottom: 20,
|
||||
borderRadius: 8, border: "1px solid #e0dcd4",
|
||||
background: "#faf8f5", fontSize: "0.9rem",
|
||||
fontFamily: "Outfit, sans-serif", color: "#1a1a1a",
|
||||
outline: "none", boxSizing: "border-box",
|
||||
}}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
|
||||
/>
|
||||
|
||||
<div style={{ fontSize: "0.75rem", color: "#a09a90", marginBottom: 20, lineHeight: 1.5, padding: "12px 14px", background: "#faf8f5", borderRadius: 8, border: "1px solid #f0ece4" }}>
|
||||
Atlas will clone your repo, read key files, and build a full architecture map — tech stack, routes, database, auth, and third-party integrations. Tokens are used only for cloning and are not stored.
|
||||
</div>
|
||||
|
||||
<PrimaryButton onClick={handleCreate} disabled={!canCreate} loading={loading}>
|
||||
Import & map →
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
106
components/project-creation/CreateProjectFlow.tsx
Normal file
106
components/project-creation/CreateProjectFlow.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { TypeSelector } from "./TypeSelector";
|
||||
import { FreshIdeaSetup } from "./FreshIdeaSetup";
|
||||
import { ChatImportSetup } from "./ChatImportSetup";
|
||||
import { CodeImportSetup } from "./CodeImportSetup";
|
||||
import { MigrateSetup } from "./MigrateSetup";
|
||||
|
||||
export type CreationMode = "fresh" | "chat-import" | "code-import" | "migration";
|
||||
|
||||
interface CreateProjectFlowProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
workspace: string;
|
||||
}
|
||||
|
||||
type Step = "select-type" | "setup";
|
||||
|
||||
export function CreateProjectFlow({ open, onOpenChange, workspace }: CreateProjectFlowProps) {
|
||||
const [step, setStep] = useState<Step>("select-type");
|
||||
const [mode, setMode] = useState<CreationMode | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setStep("select-type");
|
||||
setMode(null);
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onOpenChange(false); };
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [open, onOpenChange]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
const handleSelectType = (selected: CreationMode) => {
|
||||
setMode(selected);
|
||||
setStep("setup");
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setStep("select-type");
|
||||
setMode(null);
|
||||
};
|
||||
|
||||
const setupProps = { workspace, onClose: () => onOpenChange(false), onBack: handleBack };
|
||||
|
||||
return createPortal(
|
||||
<>
|
||||
<style>{`
|
||||
@keyframes vibn-fadeIn { from { opacity:0; } to { opacity:1; } }
|
||||
@keyframes vibn-slideUp { from { opacity:0; transform:translateY(14px); } to { opacity:1; transform:translateY(0); } }
|
||||
@keyframes vibn-spin { to { transform:rotate(360deg); } }
|
||||
`}</style>
|
||||
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
onClick={() => onOpenChange(false)}
|
||||
style={{
|
||||
position: "fixed", inset: 0, zIndex: 50,
|
||||
background: "rgba(26,26,26,0.38)",
|
||||
animation: "vibn-fadeIn 0.15s ease",
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Modal container */}
|
||||
<div style={{
|
||||
position: "fixed", inset: 0, zIndex: 51,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
padding: 24, pointerEvents: "none",
|
||||
}}>
|
||||
<div
|
||||
onClick={e => e.stopPropagation()}
|
||||
style={{
|
||||
background: "#fff", borderRadius: 16,
|
||||
boxShadow: "0 12px 48px rgba(26,26,26,0.16)",
|
||||
width: "100%",
|
||||
maxWidth: step === "select-type" ? 620 : 520,
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
pointerEvents: "all",
|
||||
animation: "vibn-slideUp 0.18s cubic-bezier(0.4,0,0.2,1)",
|
||||
transition: "max-width 0.2s ease",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{step === "select-type" && (
|
||||
<TypeSelector
|
||||
onSelect={handleSelectType}
|
||||
onClose={() => onOpenChange(false)}
|
||||
/>
|
||||
)}
|
||||
{step === "setup" && mode === "fresh" && <FreshIdeaSetup {...setupProps} />}
|
||||
{step === "setup" && mode === "chat-import" && <ChatImportSetup {...setupProps} />}
|
||||
{step === "setup" && mode === "code-import" && <CodeImportSetup {...setupProps} />}
|
||||
{step === "setup" && mode === "migration" && <MigrateSetup {...setupProps} />}
|
||||
</div>
|
||||
</div>
|
||||
</>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
91
components/project-creation/FreshIdeaSetup.tsx
Normal file
91
components/project-creation/FreshIdeaSetup.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { SetupHeader, FieldLabel, TextInput, PrimaryButton, type SetupProps } from "./setup-shared";
|
||||
|
||||
export function FreshIdeaSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
const router = useRouter();
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const nameRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const canCreate = name.trim().length > 0;
|
||||
|
||||
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, "-"),
|
||||
product: { name: name.trim() },
|
||||
creationMode: "fresh",
|
||||
sourceData: { description: description.trim() },
|
||||
}),
|
||||
});
|
||||
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}/overview`);
|
||||
} catch {
|
||||
toast.error("Something went wrong");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "32px 36px 36px" }}>
|
||||
<SetupHeader
|
||||
icon="✦" label="Fresh Idea" tagline="Start from scratch"
|
||||
accent="#4a3728" onBack={onBack} onClose={onClose}
|
||||
/>
|
||||
|
||||
<FieldLabel>Project name</FieldLabel>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChange={setName}
|
||||
placeholder="e.g. Foxglove, Meridian, OpsAI…"
|
||||
onKeyDown={e => { if (e.key === "Enter" && canCreate) handleCreate(); }}
|
||||
inputRef={nameRef}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<FieldLabel>One-line description <span style={{ color: "#b5b0a6", fontWeight: 400 }}>(optional)</span></FieldLabel>
|
||||
<input
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="A short description to kick off the conversation"
|
||||
style={{
|
||||
width: "100%", padding: "11px 14px", marginBottom: 20,
|
||||
borderRadius: 8, border: "1px solid #e0dcd4",
|
||||
background: "#faf8f5", fontSize: "0.9rem",
|
||||
fontFamily: "Outfit, sans-serif", color: "#1a1a1a",
|
||||
outline: "none", boxSizing: "border-box",
|
||||
}}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
|
||||
/>
|
||||
|
||||
<div style={{ fontSize: "0.75rem", color: "#a09a90", marginBottom: 20, lineHeight: 1.5, padding: "12px 14px", background: "#faf8f5", borderRadius: 8, border: "1px solid #f0ece4" }}>
|
||||
Atlas will guide you through 6 discovery phases — Big Picture, Users, Features, Business Model, Screens, and Risks — building your product plan as you go.
|
||||
</div>
|
||||
|
||||
<PrimaryButton onClick={handleCreate} disabled={!canCreate} loading={loading}>
|
||||
Start with Atlas →
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
159
components/project-creation/MigrateSetup.tsx
Normal file
159
components/project-creation/MigrateSetup.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { SetupHeader, FieldLabel, TextInput, PrimaryButton, type SetupProps } from "./setup-shared";
|
||||
|
||||
const HOSTING_OPTIONS = [
|
||||
{ value: "", label: "Select hosting provider" },
|
||||
{ value: "vercel", label: "Vercel" },
|
||||
{ value: "aws", label: "AWS (EC2 / ECS / Elastic Beanstalk)" },
|
||||
{ value: "heroku", label: "Heroku" },
|
||||
{ value: "digitalocean", label: "DigitalOcean (Droplet / App Platform)" },
|
||||
{ value: "gcp", label: "Google Cloud Platform" },
|
||||
{ value: "azure", label: "Microsoft Azure" },
|
||||
{ value: "railway", label: "Railway" },
|
||||
{ value: "render", label: "Render" },
|
||||
{ value: "netlify", label: "Netlify" },
|
||||
{ value: "self-hosted", label: "Self-hosted / VPS" },
|
||||
{ value: "other", label: "Other" },
|
||||
];
|
||||
|
||||
export function MigrateSetup({ workspace, onClose, onBack }: SetupProps) {
|
||||
const router = useRouter();
|
||||
const [name, setName] = useState("");
|
||||
const [repoUrl, setRepoUrl] = useState("");
|
||||
const [liveUrl, setLiveUrl] = useState("");
|
||||
const [hosting, setHosting] = useState("");
|
||||
const [pat, setPat] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const isValidRepo = repoUrl.trim().startsWith("http");
|
||||
const isValidLive = liveUrl.trim().startsWith("http");
|
||||
const canCreate = name.trim().length > 0 && (isValidRepo || isValidLive);
|
||||
|
||||
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, "-"),
|
||||
product: { name: name.trim() },
|
||||
creationMode: "migration",
|
||||
sourceData: {
|
||||
repoUrl: repoUrl.trim() || undefined,
|
||||
liveUrl: liveUrl.trim() || undefined,
|
||||
hosting: hosting || undefined,
|
||||
pat: pat.trim() || undefined,
|
||||
},
|
||||
}),
|
||||
});
|
||||
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}/overview`);
|
||||
} catch {
|
||||
toast.error("Something went wrong");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: "32px 36px 36px" }}>
|
||||
<SetupHeader
|
||||
icon="⇢" label="Migrate Product" tagline="Move an existing product"
|
||||
accent="#4a2a5a" onBack={onBack} onClose={onClose}
|
||||
/>
|
||||
|
||||
<FieldLabel>Product name</FieldLabel>
|
||||
<TextInput
|
||||
value={name}
|
||||
onChange={setName}
|
||||
placeholder="What is this product called?"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<FieldLabel>
|
||||
Repository URL{" "}
|
||||
<span style={{ color: "#b5b0a6", fontWeight: 400 }}>(recommended)</span>
|
||||
</FieldLabel>
|
||||
<TextInput
|
||||
value={repoUrl}
|
||||
onChange={setRepoUrl}
|
||||
placeholder="https://github.com/yourorg/your-repo"
|
||||
/>
|
||||
|
||||
<FieldLabel>
|
||||
Live URL{" "}
|
||||
<span style={{ color: "#b5b0a6", fontWeight: 400 }}>(optional)</span>
|
||||
</FieldLabel>
|
||||
<TextInput
|
||||
value={liveUrl}
|
||||
onChange={setLiveUrl}
|
||||
placeholder="https://yourproduct.com"
|
||||
/>
|
||||
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12, marginBottom: 4 }}>
|
||||
<div>
|
||||
<FieldLabel>Hosting provider</FieldLabel>
|
||||
<select
|
||||
value={hosting}
|
||||
onChange={e => setHosting(e.target.value)}
|
||||
style={{
|
||||
width: "100%", padding: "11px 14px", marginBottom: 16,
|
||||
borderRadius: 8, border: "1px solid #e0dcd4",
|
||||
background: "#faf8f5", fontSize: "0.88rem",
|
||||
fontFamily: "Outfit, sans-serif", color: hosting ? "#1a1a1a" : "#a09a90",
|
||||
outline: "none", boxSizing: "border-box", appearance: "none",
|
||||
backgroundImage: `url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23a09a90' strokeWidth='1.5' strokeLinecap='round' strokeLinejoin='round'/%3E%3C/svg%3E")`,
|
||||
backgroundRepeat: "no-repeat", backgroundPosition: "right 12px center",
|
||||
}}
|
||||
>
|
||||
{HOSTING_OPTIONS.map(o => (
|
||||
<option key={o.value} value={o.value}>{o.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabel>
|
||||
PAT{" "}<span style={{ color: "#b5b0a6", fontWeight: 400 }}>(private repos)</span>
|
||||
</FieldLabel>
|
||||
<input
|
||||
type="password"
|
||||
value={pat}
|
||||
onChange={e => setPat(e.target.value)}
|
||||
placeholder="ghp_…"
|
||||
style={{
|
||||
width: "100%", padding: "11px 14px", marginBottom: 16,
|
||||
borderRadius: 8, border: "1px solid #e0dcd4",
|
||||
background: "#faf8f5", fontSize: "0.9rem",
|
||||
fontFamily: "Outfit, sans-serif", color: "#1a1a1a",
|
||||
outline: "none", boxSizing: "border-box",
|
||||
}}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ fontSize: "0.75rem", color: "#a09a90", marginBottom: 20, lineHeight: 1.5, padding: "12px 14px", background: "#faf8f5", borderRadius: 8, border: "1px solid #f0ece4" }}>
|
||||
<strong style={{ color: "#4a2a5a" }}>Non-destructive.</strong> Atlas builds a full audit and migration plan. Your existing product stays live throughout the entire migration process.
|
||||
</div>
|
||||
|
||||
<PrimaryButton onClick={handleCreate} disabled={!canCreate} loading={loading}>
|
||||
Start migration plan →
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
145
components/project-creation/TypeSelector.tsx
Normal file
145
components/project-creation/TypeSelector.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
"use client";
|
||||
|
||||
import type { CreationMode } from "./CreateProjectFlow";
|
||||
|
||||
interface TypeSelectorProps {
|
||||
onSelect: (mode: CreationMode) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const FLOW_TYPES: {
|
||||
id: CreationMode;
|
||||
icon: string;
|
||||
label: string;
|
||||
tagline: string;
|
||||
desc: string;
|
||||
accent: string;
|
||||
}[] = [
|
||||
{
|
||||
id: "fresh",
|
||||
icon: "✦",
|
||||
label: "Fresh Idea",
|
||||
tagline: "Start from scratch",
|
||||
desc: "Talk through your idea with Atlas. We'll explore it together and shape it into a full product plan.",
|
||||
accent: "#4a3728",
|
||||
},
|
||||
{
|
||||
id: "chat-import",
|
||||
icon: "⌁",
|
||||
label: "Import Chats",
|
||||
tagline: "You've been thinking",
|
||||
desc: "Paste conversations from ChatGPT or Claude. Atlas extracts your decisions, ideas, and open questions.",
|
||||
accent: "#2e5a4a",
|
||||
},
|
||||
{
|
||||
id: "code-import",
|
||||
icon: "⌘",
|
||||
label: "Import Code",
|
||||
tagline: "Already have a repo",
|
||||
desc: "Point Atlas at your GitHub or Bitbucket repo. We'll map your stack and show what's missing.",
|
||||
accent: "#1a3a5c",
|
||||
},
|
||||
{
|
||||
id: "migration",
|
||||
icon: "⇢",
|
||||
label: "Migrate Product",
|
||||
tagline: "Move an existing product",
|
||||
desc: "Bring your live product into the VIBN infrastructure. Atlas builds a safe, phased migration plan.",
|
||||
accent: "#4a2a5a",
|
||||
},
|
||||
];
|
||||
|
||||
export function TypeSelector({ onSelect, onClose }: TypeSelectorProps) {
|
||||
return (
|
||||
<div style={{ padding: "32px 36px 36px" }}>
|
||||
{/* Header */}
|
||||
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", marginBottom: 28 }}>
|
||||
<div>
|
||||
<h2 style={{
|
||||
fontFamily: "Newsreader, serif", fontSize: "1.4rem", fontWeight: 400,
|
||||
color: "#1a1a1a", margin: 0, marginBottom: 4,
|
||||
}}>
|
||||
Start a new project
|
||||
</h2>
|
||||
<p style={{ fontSize: "0.78rem", color: "#a09a90", margin: 0 }}>
|
||||
How would you like to begin?
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: "none", border: "none", cursor: "pointer",
|
||||
color: "#b5b0a6", fontSize: "1.2rem", lineHeight: 1,
|
||||
padding: "2px 5px", borderRadius: 4,
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = "#6b6560")}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = "#b5b0a6")}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Type cards */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
|
||||
{FLOW_TYPES.map(type => (
|
||||
<button
|
||||
key={type.id}
|
||||
onClick={() => onSelect(type.id)}
|
||||
style={{
|
||||
display: "flex", flexDirection: "column", alignItems: "flex-start",
|
||||
gap: 0, padding: "20px", borderRadius: 12, textAlign: "left",
|
||||
border: "1px solid #e8e4dc",
|
||||
background: "#faf8f5",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.14s",
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
onMouseEnter={e => {
|
||||
e.currentTarget.style.borderColor = "#d0ccc4";
|
||||
e.currentTarget.style.background = "#fff";
|
||||
e.currentTarget.style.boxShadow = "0 2px 12px rgba(26,26,26,0.07)";
|
||||
}}
|
||||
onMouseLeave={e => {
|
||||
e.currentTarget.style.borderColor = "#e8e4dc";
|
||||
e.currentTarget.style.background = "#faf8f5";
|
||||
e.currentTarget.style.boxShadow = "none";
|
||||
}}
|
||||
>
|
||||
{/* Icon */}
|
||||
<div style={{
|
||||
width: 36, height: 36, borderRadius: 9, marginBottom: 14,
|
||||
background: `${type.accent}10`,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: "1.1rem", color: type.accent,
|
||||
}}>
|
||||
{type.icon}
|
||||
</div>
|
||||
|
||||
{/* Label + tagline */}
|
||||
<div style={{ fontSize: "0.88rem", fontWeight: 700, color: "#1a1a1a", marginBottom: 2 }}>
|
||||
{type.label}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.68rem", fontWeight: 600, color: type.accent, letterSpacing: "0.03em", marginBottom: 8, textTransform: "uppercase" }}>
|
||||
{type.tagline}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div style={{ fontSize: "0.75rem", color: "#8a8478", lineHeight: 1.5 }}>
|
||||
{type.desc}
|
||||
</div>
|
||||
|
||||
{/* Arrow */}
|
||||
<div style={{
|
||||
position: "absolute", right: 16, bottom: 16,
|
||||
fontSize: "0.85rem", color: "#c5c0b8",
|
||||
}}>
|
||||
→
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
153
components/project-creation/setup-shared.tsx
Normal file
153
components/project-creation/setup-shared.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, CSSProperties } from "react";
|
||||
|
||||
export interface SetupProps {
|
||||
workspace: string;
|
||||
onClose: () => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
// Shared modal header
|
||||
export function SetupHeader({
|
||||
icon,
|
||||
label,
|
||||
tagline,
|
||||
accent,
|
||||
onBack,
|
||||
onClose,
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
tagline: string;
|
||||
accent: string;
|
||||
onBack: () => void;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", marginBottom: 28 }}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
|
||||
<button
|
||||
onClick={onBack}
|
||||
style={{
|
||||
background: "none", border: "none", cursor: "pointer",
|
||||
color: "#b5b0a6", fontSize: "1rem", padding: "3px 5px",
|
||||
borderRadius: 4, lineHeight: 1, flexShrink: 0,
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = "#1a1a1a")}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = "#b5b0a6")}
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<div>
|
||||
<h2 style={{
|
||||
fontFamily: "Newsreader, serif", fontSize: "1.3rem", fontWeight: 400,
|
||||
color: "#1a1a1a", margin: 0, marginBottom: 3,
|
||||
}}>
|
||||
{label}
|
||||
</h2>
|
||||
<p style={{ fontSize: "0.72rem", fontWeight: 600, color: accent, textTransform: "uppercase", letterSpacing: "0.04em", margin: 0 }}>
|
||||
{tagline}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
style={{
|
||||
background: "none", border: "none", cursor: "pointer",
|
||||
color: "#b5b0a6", fontSize: "1.2rem", lineHeight: 1,
|
||||
padding: "2px 5px", borderRadius: 4, flexShrink: 0,
|
||||
}}
|
||||
onMouseEnter={e => (e.currentTarget.style.color = "#6b6560")}
|
||||
onMouseLeave={e => (e.currentTarget.style.color = "#b5b0a6")}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FieldLabel({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<label style={{ display: "block", fontSize: "0.72rem", fontWeight: 600, color: "#6b6560", marginBottom: 6, letterSpacing: "0.02em" }}>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function TextInput({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
onKeyDown,
|
||||
autoFocus,
|
||||
inputRef,
|
||||
}: {
|
||||
value: string;
|
||||
onChange: (v: string) => void;
|
||||
placeholder?: string;
|
||||
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
autoFocus?: boolean;
|
||||
inputRef?: React.RefObject<HTMLInputElement>;
|
||||
}) {
|
||||
const base: CSSProperties = {
|
||||
width: "100%", padding: "11px 14px", marginBottom: 16,
|
||||
borderRadius: 8, border: "1px solid #e0dcd4",
|
||||
background: "#faf8f5", fontSize: "0.9rem",
|
||||
fontFamily: "Outfit, sans-serif", color: "#1a1a1a",
|
||||
outline: "none", boxSizing: "border-box",
|
||||
};
|
||||
return (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
style={base}
|
||||
onFocus={e => (e.currentTarget.style.borderColor = "#1a1a1a")}
|
||||
onBlur={e => (e.currentTarget.style.borderColor = "#e0dcd4")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function PrimaryButton({
|
||||
onClick,
|
||||
disabled,
|
||||
loading,
|
||||
children,
|
||||
}: {
|
||||
onClick: () => void;
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const active = !disabled && !loading;
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={!active}
|
||||
style={{
|
||||
width: "100%", padding: "12px",
|
||||
borderRadius: 8, border: "none",
|
||||
background: active ? "#1a1a1a" : "#e0dcd4",
|
||||
color: active ? "#fff" : "#b5b0a6",
|
||||
fontSize: "0.88rem", fontWeight: 600,
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
cursor: active ? "pointer" : "not-allowed",
|
||||
display: "flex", alignItems: "center", justifyContent: "center", gap: 8,
|
||||
}}
|
||||
onMouseEnter={e => { if (active) e.currentTarget.style.opacity = "0.85"; }}
|
||||
onMouseLeave={e => { e.currentTarget.style.opacity = "1"; }}
|
||||
>
|
||||
{loading ? (
|
||||
<>
|
||||
<span style={{ width: 14, height: 14, borderRadius: "50%", border: "2px solid #fff4", borderTopColor: "#fff", animation: "vibn-spin 0.7s linear infinite", display: "inline-block" }} />
|
||||
Creating…
|
||||
</>
|
||||
) : children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user