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:
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