Theia rip-out: - Delete app/api/theia-auth/route.ts (Traefik ForwardAuth shim) - Delete app/api/projects/[projectId]/workspace/route.ts and app/api/projects/prewarm/route.ts (Cloud Run Theia provisioning) - Delete lib/cloud-run-workspace.ts and lib/coolify-workspace.ts - Strip provisionTheiaWorkspace + theiaWorkspaceUrl/theiaAppUuid/ theiaError from app/api/projects/create/route.ts response - Remove Theia callbackUrl branch in app/auth/page.tsx - Drop "Open in Theia" button + xterm/Theia PTY copy in build/page.tsx - Drop theiaWorkspaceUrl from deployment/page.tsx Project type - Strip Theia IDE line + theia-code-os from advisor + agent-chat context strings - Scrub Theia mention from lib/auth/workspace-auth.ts comment P5.1 (custom apex domains + DNS): - lib/coolify.ts + lib/opensrs.ts: nameserver normalization, OpenSRS XML auth, Cloud DNS plumbing - scripts/smoke-attach-e2e.ts: full prod GCP + sandbox OpenSRS + prod Coolify smoke covering register/zone/A/NS/PATCH/cleanup In-progress (Justine onboarding/build, MVP setup, agent telemetry): - New (justine)/stories, project (home) layouts, mvp-setup, run, tasks routes + supporting components - Project shell + sidebar + nav refactor for the Stackless palette - Agent session API hardening (sessions, events, stream, approve, retry, stop) + atlas-chat, advisor, design-surfaces refresh - New scripts/sync-db-url-from-coolify.mjs + scripts/prisma-db-push.mjs + docker-compose.local-db.yml for local Prisma workflows - lib/dev-bypass.ts, lib/chat-context-refs.ts, lib/prd-sections.ts - Misc: stories CSS, debug/prisma route, modal-theme, BuildLivePlanPanel Made-with: Cursor
1069 lines
38 KiB
TypeScript
1069 lines
38 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useRef, useState } from "react";
|
||
import Link from "next/link";
|
||
import { useRouter } from "next/navigation";
|
||
import { JM, JV } from "@/components/project-creation/modal-theme";
|
||
|
||
const STEPS = [
|
||
{ l: "Creating Gitea repository", d: "Setting up version control for your project" },
|
||
{ l: "Scaffolding the app", d: "Next.js · TypeScript · Tailwind CSS" },
|
||
{ l: "Setting up your database", d: "PostgreSQL + schema based on your product plan" },
|
||
{ l: "Building sign up & login", d: "Email + Google + GitHub OAuth" },
|
||
{ l: "Wiring payments", d: "Stripe checkout, webhooks, billing portal" },
|
||
{ l: "Generating app pages", d: "Dashboard, settings, onboarding, invite flow" },
|
||
{ l: "Applying your design", d: "Theme applied across all pages" },
|
||
{ l: "Building marketing website", d: "SEO-ready marketing surface" },
|
||
{ l: "Setting up email", d: "Welcome, password reset, and marketing templates" },
|
||
{ l: "Pushing to Gitea", d: "Full codebase committed and pushed" },
|
||
{ l: "Deploying via Coolify", d: "Building Docker image, deploying to your servers" },
|
||
{ l: "Running health checks", d: "Verifying pages, auth, and payments are live" },
|
||
] as const;
|
||
|
||
type PhaseRowProps = {
|
||
done: boolean;
|
||
active: boolean;
|
||
label: string;
|
||
sub?: string;
|
||
onClick?: () => void;
|
||
};
|
||
|
||
function PhaseRow({ done, active, label, sub, onClick }: PhaseRowProps) {
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={onClick}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 9,
|
||
padding: "9px 10px",
|
||
borderRadius: 8,
|
||
border: "none",
|
||
background: active ? "#fafaff" : "transparent",
|
||
cursor: onClick ? "pointer" : "default",
|
||
width: "100%",
|
||
textAlign: "left",
|
||
fontFamily: JM.fontSans,
|
||
}}
|
||
onMouseEnter={e => {
|
||
if (onClick && !active) (e.currentTarget as HTMLElement).style.background = "#F5F3FF";
|
||
}}
|
||
onMouseLeave={e => {
|
||
if (!active) (e.currentTarget as HTMLElement).style.background = "transparent";
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 20,
|
||
height: 20,
|
||
borderRadius: "50%",
|
||
background: done ? JM.primaryGradient : active ? JM.indigo : "#e5e7eb",
|
||
color: "#fff",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
flexShrink: 0,
|
||
fontSize: 10,
|
||
fontWeight: 700,
|
||
}}
|
||
>
|
||
{done ? "✓" : active ? "▲" : ""}
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: 12.5, fontWeight: active ? 600 : 400, color: JM.ink }}>{label}</div>
|
||
{sub && <div style={{ fontSize: 10, color: JM.muted }}>{sub}</div>}
|
||
</div>
|
||
</button>
|
||
);
|
||
}
|
||
|
||
function Confetti() {
|
||
const colors = ["#6366F1", "#818CF8", "#4338CA", "#A5B4FC", "#C7D2FE", "#FCD34D", "#34D399", "#60A5FA"];
|
||
const pieces = Array.from({ length: 90 }, (_, i) => ({
|
||
i,
|
||
color: colors[i % colors.length],
|
||
left: Math.random() * 100,
|
||
delay: Math.random() * 1.2,
|
||
dur: Math.random() * 2.5 + 2,
|
||
size: Math.random() * 9 + 4,
|
||
xDrift: (Math.random() - 0.5) * 200,
|
||
rot: Math.random() * 360,
|
||
br: ["50%", "3px", "0"][Math.floor(Math.random() * 3)],
|
||
}));
|
||
return (
|
||
<div
|
||
style={{
|
||
position: "fixed",
|
||
top: 0,
|
||
left: 0,
|
||
width: "100%",
|
||
height: "100%",
|
||
pointerEvents: "none",
|
||
zIndex: 9999,
|
||
overflow: "hidden",
|
||
}}
|
||
>
|
||
<style>{`
|
||
@keyframes justineConfettiFall {
|
||
0% { transform: translateY(-20px) rotate(0deg); opacity: 1; }
|
||
80% { opacity: 1; }
|
||
100% { transform: translateY(105vh) rotate(800deg); opacity: 0; }
|
||
}
|
||
`}</style>
|
||
{pieces.map(p => (
|
||
<div
|
||
key={p.i}
|
||
style={{
|
||
position: "absolute",
|
||
top: -12,
|
||
left: `${p.left}%`,
|
||
width: p.size,
|
||
height: p.size * (Math.random() * 0.6 + 0.4),
|
||
background: p.color,
|
||
borderRadius: p.br,
|
||
animation: `justineConfettiFall ${p.dur}s ${p.delay}s ease-in forwards`,
|
||
transform: `translateX(${p.xDrift}px) rotate(${p.rot}deg)`,
|
||
}}
|
||
/>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export interface BuildMvpJustineV2Props {
|
||
workspace: string;
|
||
projectId: string;
|
||
projectName: string;
|
||
giteaRepo?: string;
|
||
/** First webapp surface label + theme if any */
|
||
designFeel?: string;
|
||
designStructure?: string;
|
||
accentLabel?: string;
|
||
accentHex?: string;
|
||
websiteVoice?: string;
|
||
websiteStyle?: string;
|
||
topicsLine?: string;
|
||
pageColumns?: { title: string; pages: string[] }[];
|
||
onSwitchToPreview: () => void;
|
||
}
|
||
|
||
export function BuildMvpJustineV2({
|
||
workspace,
|
||
projectId,
|
||
projectName,
|
||
giteaRepo,
|
||
designFeel = "Friendly",
|
||
designStructure = "Clean",
|
||
accentLabel = "Indigo",
|
||
accentHex = "#6366F1",
|
||
websiteVoice = "Friendly · Balanced · Warm",
|
||
websiteStyle = "Editorial",
|
||
topicsLine = "The problem · Who it's for · Why now",
|
||
pageColumns = [
|
||
{ title: "Public", pages: ["Landing page", "Pricing", "About", "Blog"] },
|
||
{ title: "Auth", pages: ["Sign up", "Log in", "Forgot password"] },
|
||
{ title: "App", pages: ["Dashboard", "Onboarding", "Settings"] },
|
||
{ title: "Payments", pages: ["Checkout", "Success", "Manage subscription"] },
|
||
],
|
||
onSwitchToPreview,
|
||
}: BuildMvpJustineV2Props) {
|
||
const router = useRouter();
|
||
const [uiPhase, setUiPhase] = useState<"review" | "progress" | "done">("review");
|
||
const [curStep, setCurStep] = useState(0);
|
||
const [building, setBuilding] = useState(false);
|
||
const [showConfetti, setShowConfetti] = useState(false);
|
||
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||
|
||
const giteaWebBase = process.env.NEXT_PUBLIC_GITEA_WEB_URL ?? "https://git.vibnai.com";
|
||
const giteaHref = giteaRepo ? `${giteaWebBase}/${giteaRepo}` : giteaWebBase;
|
||
|
||
const clearBuildInterval = useCallback(() => {
|
||
if (intervalRef.current) {
|
||
clearInterval(intervalRef.current);
|
||
intervalRef.current = null;
|
||
}
|
||
}, []);
|
||
|
||
useEffect(() => () => clearBuildInterval(), [clearBuildInterval]);
|
||
|
||
const startBuild = () => {
|
||
if (building) return;
|
||
setBuilding(true);
|
||
setTimeout(() => {
|
||
setUiPhase("progress");
|
||
setCurStep(0);
|
||
intervalRef.current = setInterval(() => {
|
||
setCurStep(c => {
|
||
const next = c + 1;
|
||
if (next >= STEPS.length) {
|
||
clearBuildInterval();
|
||
setUiPhase("done");
|
||
setBuilding(false);
|
||
setShowConfetti(true);
|
||
setTimeout(() => setShowConfetti(false), 5000);
|
||
return STEPS.length;
|
||
}
|
||
return next;
|
||
});
|
||
}, 700);
|
||
}, 400);
|
||
};
|
||
|
||
const renderStepRows = () =>
|
||
STEPS.map((s, i) => {
|
||
const done = i < curStep;
|
||
const active = i === curStep && uiPhase === "progress";
|
||
return (
|
||
<div
|
||
key={s.l}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 12,
|
||
padding: "10px 15px",
|
||
borderBottom: i < STEPS.length - 1 ? `1px solid ${JM.border}` : "none",
|
||
background: active ? JV.violetTint : "transparent",
|
||
transition: "background 0.3s",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 20,
|
||
height: 20,
|
||
borderRadius: "50%",
|
||
background: done
|
||
? JM.primaryGradient
|
||
: active
|
||
? "linear-gradient(135deg,#4338CA,#6C7CFF)"
|
||
: "#d3d1c7",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
{done && <span style={{ color: "#fff", fontSize: 9, fontWeight: 900 }}>✓</span>}
|
||
{active && (
|
||
<span
|
||
style={{
|
||
color: "#fff",
|
||
fontSize: 8,
|
||
animation: "spin 1s linear infinite",
|
||
display: "inline-block",
|
||
}}
|
||
>
|
||
◎
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<div
|
||
style={{
|
||
fontSize: 12.5,
|
||
fontWeight: active ? 600 : 400,
|
||
color: done ? JM.muted : active ? JM.ink : "#b4b2a9",
|
||
}}
|
||
>
|
||
{s.l}
|
||
</div>
|
||
{(done || active) && (
|
||
<div style={{ fontSize: 11, color: JM.mid, marginTop: 1 }}>{s.d}</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
});
|
||
|
||
return (
|
||
<>
|
||
<style>{`
|
||
@keyframes spin { to { transform: rotate(360deg); } }
|
||
@keyframes grad-flow {
|
||
0% { background-position: 0% 50%; }
|
||
50% { background-position: 100% 50%; }
|
||
100% { background-position: 0% 50%; }
|
||
}
|
||
.justine-grad-anim {
|
||
background: linear-gradient(270deg,#1A1A2E,#4338CA,#6366F1,#A5B4FC,#6366F1,#4338CA,#1A1A2E);
|
||
background-size: 400% 400%;
|
||
-webkit-background-clip: text;
|
||
background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
animation: grad-flow 4s ease infinite;
|
||
}
|
||
.justine-grad-title {
|
||
background: linear-gradient(135deg,#1A1A2E 0%,#2E2A5E 30%,#4338CA 65%,#6366F1 100%);
|
||
-webkit-background-clip: text;
|
||
background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
`}</style>
|
||
{showConfetti && <Confetti />}
|
||
|
||
<div
|
||
style={{
|
||
width: 200,
|
||
flexShrink: 0,
|
||
background: "#ffffff",
|
||
borderRight: `1px solid ${JM.border}`,
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
padding: "18px 12px",
|
||
overflow: "hidden",
|
||
}}
|
||
>
|
||
<div style={{ padding: "0 6px", marginBottom: 26 }}>
|
||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 6 }}>
|
||
<div
|
||
style={{
|
||
width: 26,
|
||
height: 26,
|
||
background: JM.primaryGradient,
|
||
borderRadius: 6,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
<span style={{ fontSize: 13, fontWeight: 700, color: "#fff" }}>V</span>
|
||
</div>
|
||
<span style={{ fontSize: 16, fontWeight: 700, color: JM.ink, letterSpacing: "-0.02em" }}>vibn</span>
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize: 11,
|
||
fontWeight: 500,
|
||
color: JM.muted,
|
||
paddingLeft: 34,
|
||
whiteSpace: "nowrap",
|
||
overflow: "hidden",
|
||
textOverflow: "ellipsis",
|
||
}}
|
||
>
|
||
{projectName}
|
||
</div>
|
||
</div>
|
||
<div
|
||
style={{
|
||
fontSize: 9.5,
|
||
fontWeight: 600,
|
||
letterSpacing: "0.08em",
|
||
textTransform: "uppercase",
|
||
color: JM.muted,
|
||
padding: "0 6px",
|
||
marginBottom: 8,
|
||
}}
|
||
>
|
||
MVP Setup
|
||
</div>
|
||
<div style={{ display: "flex", flexDirection: "column", gap: 2, flex: 1, minHeight: 0 }}>
|
||
<PhaseRow
|
||
done
|
||
active={false}
|
||
label="Describe"
|
||
onClick={() => router.push(`/${workspace}/project/${projectId}/overview`)}
|
||
/>
|
||
<PhaseRow
|
||
done
|
||
active={false}
|
||
label="Architect"
|
||
onClick={() => router.push(`/${workspace}/project/${projectId}/tasks`)}
|
||
/>
|
||
<PhaseRow
|
||
done
|
||
active={false}
|
||
label="Design"
|
||
onClick={() => router.push(`/${workspace}/project/${projectId}/design`)}
|
||
/>
|
||
<PhaseRow
|
||
done
|
||
active={false}
|
||
label="Website"
|
||
onClick={() => router.push(`/${workspace}/project/${projectId}/growth`)}
|
||
/>
|
||
<PhaseRow done={false} active label="Build MVP" sub="Review & launch" />
|
||
</div>
|
||
<div style={{ borderTop: `1px solid ${JM.border}`, marginTop: 14, paddingTop: 12 }}>
|
||
<Link
|
||
href={`/${workspace}/projects`}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
gap: 7,
|
||
width: "100%",
|
||
background: JV.violetTint,
|
||
border: `1px solid ${JV.bubbleAiBorder}`,
|
||
borderRadius: 8,
|
||
padding: "9px 10px",
|
||
fontFamily: JM.fontSans,
|
||
textDecoration: "none",
|
||
}}
|
||
>
|
||
<span style={{ fontSize: 12, fontWeight: 600, color: JM.indigo }}>Save & go to dashboard</span>
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
|
||
<div style={{ flex: 1, overflowY: "auto", minWidth: 0 }}>
|
||
{uiPhase === "review" && (
|
||
<div style={{ padding: "28px 32px", maxWidth: 680, margin: "0 auto" }}>
|
||
<div style={{ fontSize: 22, fontWeight: 700, color: JM.ink, marginBottom: 6 }}>Ready to build</div>
|
||
<p style={{ fontSize: 13.5, color: JM.muted, marginBottom: 22, lineHeight: 1.55 }}>
|
||
Review everything below. Once you hit Build, AI codes your full product and deploys it.
|
||
</p>
|
||
|
||
<div
|
||
style={{
|
||
background: "#fff",
|
||
border: `1px solid ${JM.border}`,
|
||
borderRadius: 12,
|
||
overflow: "hidden",
|
||
marginBottom: 14,
|
||
}}
|
||
>
|
||
<div style={{ padding: "12px 18px", borderBottom: `1px solid ${JM.border}` }}>
|
||
<span
|
||
style={{
|
||
fontSize: 10.5,
|
||
fontWeight: 700,
|
||
color: JM.muted,
|
||
textTransform: "uppercase",
|
||
letterSpacing: "0.06em",
|
||
}}
|
||
>
|
||
What's being built
|
||
</span>
|
||
</div>
|
||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
|
||
{(
|
||
[
|
||
{ icon: "⛓", k: "Sign up & login", v: "Email + social login", br: true, bb: true },
|
||
{ icon: "$", k: "Payments", v: "Subscription billing", br: false, bb: true },
|
||
{ icon: "✉", k: "Email", v: "Transactional + marketing", br: true, bb: true },
|
||
{ icon: "◧", k: "Product style", v: "Clean & focused", br: false, bb: true },
|
||
{ icon: "◉", k: "Website style", v: websiteStyle, br: true, bb: false },
|
||
{ icon: "≡", k: "Campaign topics", v: topicsLine, br: false, bb: false },
|
||
] as const
|
||
).map(cell => (
|
||
<div
|
||
key={cell.k}
|
||
style={{
|
||
padding: "13px 18px",
|
||
borderRight: cell.br ? `1px solid ${JM.border}` : undefined,
|
||
borderBottom: cell.bb ? `1px solid ${JM.border}` : undefined,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 10,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 26,
|
||
height: 26,
|
||
borderRadius: 7,
|
||
background: JV.violetTint,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
fontSize: 12,
|
||
color: JM.indigo,
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
{cell.icon}
|
||
</div>
|
||
<div>
|
||
<div
|
||
style={{
|
||
fontSize: 10,
|
||
color: JM.muted,
|
||
textTransform: "uppercase",
|
||
letterSpacing: "0.05em",
|
||
marginBottom: 2,
|
||
}}
|
||
>
|
||
{cell.k}
|
||
</div>
|
||
<div style={{ fontSize: 13, fontWeight: 600, color: JM.ink }}>{cell.v}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
style={{
|
||
background: "#fff",
|
||
border: `1px solid ${JM.border}`,
|
||
borderRadius: 12,
|
||
overflow: "hidden",
|
||
marginBottom: 14,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
padding: "12px 18px",
|
||
borderBottom: `1px solid ${JM.border}`,
|
||
display: "flex",
|
||
justifyContent: "space-between",
|
||
}}
|
||
>
|
||
<span
|
||
style={{
|
||
fontSize: 10.5,
|
||
fontWeight: 700,
|
||
color: JM.muted,
|
||
textTransform: "uppercase",
|
||
letterSpacing: "0.06em",
|
||
}}
|
||
>
|
||
Pages
|
||
</span>
|
||
<span style={{ fontSize: 12, color: JM.muted }}>
|
||
{pageColumns.reduce((n, c) => n + c.pages.length, 0)} pages total
|
||
</span>
|
||
</div>
|
||
<div style={{ padding: "16px 18px", display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 0 }}>
|
||
{pageColumns.map((col, ci) => (
|
||
<div
|
||
key={col.title}
|
||
style={{
|
||
padding: "0 14px 0 0",
|
||
borderRight: ci < pageColumns.length - 1 ? `1px solid ${JM.border}` : undefined,
|
||
marginRight: ci < pageColumns.length - 1 ? 14 : 0,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
fontSize: 9.5,
|
||
fontWeight: 700,
|
||
color: "#7171b7",
|
||
textTransform: "uppercase",
|
||
letterSpacing: "0.07em",
|
||
marginBottom: 8,
|
||
}}
|
||
>
|
||
{col.title}
|
||
</div>
|
||
<div style={{ fontSize: 12.5, color: "#2c2c2a", lineHeight: 2 }}>
|
||
{col.pages.map(p => (
|
||
<span key={p}>
|
||
{p}
|
||
<br />
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
style={{
|
||
background: "#fff",
|
||
border: `1px solid ${JM.border}`,
|
||
borderRadius: 12,
|
||
overflow: "hidden",
|
||
marginBottom: 14,
|
||
}}
|
||
>
|
||
<div style={{ padding: "12px 18px", borderBottom: `1px solid ${JM.border}` }}>
|
||
<span
|
||
style={{
|
||
fontSize: 10.5,
|
||
fontWeight: 700,
|
||
color: JM.muted,
|
||
textTransform: "uppercase",
|
||
letterSpacing: "0.06em",
|
||
}}
|
||
>
|
||
Your design
|
||
</span>
|
||
</div>
|
||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr" }}>
|
||
<div
|
||
style={{
|
||
padding: "13px 18px",
|
||
borderRight: `1px solid ${JM.border}`,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 10,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 26,
|
||
height: 26,
|
||
borderRadius: 7,
|
||
background: JV.violetTint,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
fontSize: 12,
|
||
color: JM.indigo,
|
||
}}
|
||
>
|
||
◈
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: 10, color: JM.muted, textTransform: "uppercase", marginBottom: 2 }}>Feel</div>
|
||
<div style={{ fontSize: 13, fontWeight: 600, color: JM.ink }}>{designFeel}</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style={{
|
||
padding: "13px 18px",
|
||
borderRight: `1px solid ${JM.border}`,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 10,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 26,
|
||
height: 26,
|
||
borderRadius: "50%",
|
||
background: accentHex,
|
||
flexShrink: 0,
|
||
boxShadow: `0 0 0 3px #fff, 0 0 0 4px ${accentHex}44`,
|
||
}}
|
||
/>
|
||
<div>
|
||
<div style={{ fontSize: 10, color: JM.muted, textTransform: "uppercase", marginBottom: 2 }}>Accent</div>
|
||
<div style={{ fontSize: 13, fontWeight: 600, color: JM.ink }}>{accentLabel}</div>
|
||
</div>
|
||
</div>
|
||
<div style={{ padding: "13px 18px", display: "flex", alignItems: "center", gap: 10 }}>
|
||
<div
|
||
style={{
|
||
width: 26,
|
||
height: 26,
|
||
borderRadius: 7,
|
||
background: JV.violetTint,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
fontSize: 12,
|
||
color: JM.indigo,
|
||
}}
|
||
>
|
||
◇
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: 10, color: JM.muted, textTransform: "uppercase", marginBottom: 2 }}>Layout</div>
|
||
<div style={{ fontSize: 13, fontWeight: 600, color: JM.ink }}>{designStructure}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
style={{
|
||
background: "#fff",
|
||
border: `1px solid ${JM.border}`,
|
||
borderRadius: 12,
|
||
overflow: "hidden",
|
||
marginBottom: 14,
|
||
}}
|
||
>
|
||
<div style={{ padding: "12px 18px", borderBottom: `1px solid ${JM.border}` }}>
|
||
<span
|
||
style={{
|
||
fontSize: 10.5,
|
||
fontWeight: 700,
|
||
color: JM.muted,
|
||
textTransform: "uppercase",
|
||
letterSpacing: "0.06em",
|
||
}}
|
||
>
|
||
Your website
|
||
</span>
|
||
</div>
|
||
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr" }}>
|
||
<div
|
||
style={{
|
||
padding: "13px 18px",
|
||
borderRight: `1px solid ${JM.border}`,
|
||
borderBottom: `1px solid ${JM.border}`,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 10,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 26,
|
||
height: 26,
|
||
borderRadius: 7,
|
||
background: JV.violetTint,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
fontSize: 12,
|
||
color: JM.indigo,
|
||
}}
|
||
>
|
||
✦
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: 10, color: JM.muted, textTransform: "uppercase", marginBottom: 2 }}>Voice</div>
|
||
<div style={{ fontSize: 13, fontWeight: 600, color: JM.ink }}>{websiteVoice}</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style={{
|
||
padding: "13px 18px",
|
||
borderBottom: `1px solid ${JM.border}`,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 10,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 26,
|
||
height: 26,
|
||
borderRadius: 7,
|
||
background: JV.violetTint,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
fontSize: 12,
|
||
color: JM.indigo,
|
||
}}
|
||
>
|
||
⬡
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: 10, color: JM.muted, textTransform: "uppercase", marginBottom: 2 }}>
|
||
Website style
|
||
</div>
|
||
<div style={{ fontSize: 13, fontWeight: 600, color: JM.ink }}>{websiteStyle}</div>
|
||
</div>
|
||
</div>
|
||
<div
|
||
style={{
|
||
padding: "13px 18px",
|
||
gridColumn: "1 / -1",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 10,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 26,
|
||
height: 26,
|
||
borderRadius: 7,
|
||
background: JV.violetTint,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
fontSize: 12,
|
||
color: JM.indigo,
|
||
}}
|
||
>
|
||
◉
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: 10, color: JM.muted, textTransform: "uppercase", marginBottom: 2 }}>Topics</div>
|
||
<div style={{ fontSize: 13, fontWeight: 600, color: JM.ink }}>{topicsLine}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<hr style={{ border: "none", borderTop: `1px solid ${JM.border}`, margin: "28px 0" }} />
|
||
|
||
<div
|
||
style={{
|
||
background: "#fff",
|
||
border: `1px solid ${JM.border}`,
|
||
borderRadius: 16,
|
||
padding: "28px 28px 24px",
|
||
boxShadow: "0 2px 12px rgba(99,102,241,0.06), 0 0 0 1px rgba(255,255,255,0.6)",
|
||
}}
|
||
>
|
||
<div style={{ marginBottom: 24 }}>
|
||
<div
|
||
style={{
|
||
fontSize: 24,
|
||
fontWeight: 700,
|
||
color: JM.ink,
|
||
letterSpacing: "-0.01em",
|
||
marginBottom: 8,
|
||
}}
|
||
>
|
||
You're ready to build your product
|
||
</div>
|
||
<p style={{ fontSize: 14, color: JM.muted, lineHeight: 1.5, maxWidth: 520, margin: 0 }}>
|
||
Your app will be generated, your backend configured, and everything deployed to your infrastructure —
|
||
fully automated, no code needed.
|
||
</p>
|
||
</div>
|
||
<div style={{ marginTop: 26, marginBottom: 6 }}>
|
||
<div
|
||
style={{
|
||
fontSize: 11,
|
||
fontWeight: 600,
|
||
color: JM.muted,
|
||
textTransform: "uppercase",
|
||
letterSpacing: "0.08em",
|
||
marginBottom: 14,
|
||
opacity: 0.7,
|
||
}}
|
||
>
|
||
What happens next
|
||
</div>
|
||
{[
|
||
{ icon: "✦", t: "Generate UI & all pages", est: "~30s" },
|
||
{ icon: "⛁", t: "Set up database & backend", est: "~45s" },
|
||
{ icon: "⛓", t: "Connect auth, payments & email", est: "~30s" },
|
||
{ icon: "▲", t: "Deploy your app live", est: "~20s" },
|
||
].map(row => (
|
||
<div
|
||
key={row.t}
|
||
style={{
|
||
display: "flex",
|
||
alignItems: "center",
|
||
gap: 11,
|
||
padding: "9px 0",
|
||
borderBottom: `1px solid ${JM.border}`,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 24,
|
||
height: 24,
|
||
borderRadius: 6,
|
||
background: JV.violetTint,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
fontSize: 11,
|
||
color: JM.indigo,
|
||
flexShrink: 0,
|
||
opacity: 0.85,
|
||
}}
|
||
>
|
||
{row.icon}
|
||
</div>
|
||
<div style={{ flex: 1 }}>
|
||
<span style={{ fontSize: 13, fontWeight: 500, color: JM.ink }}>{row.t}</span>
|
||
</div>
|
||
<span style={{ fontSize: 12, color: "#b4b2a9" }}>{row.est}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<p style={{ fontSize: 11.5, color: JM.muted, marginBottom: 28 }}>
|
||
Takes ~2–4 minutes · All steps run in parallel
|
||
</p>
|
||
<button
|
||
type="button"
|
||
disabled={building}
|
||
onClick={startBuild}
|
||
style={{
|
||
width: "100%",
|
||
background: JM.primaryGradient,
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: 12,
|
||
padding: "16px 24px",
|
||
fontFamily: JM.fontSans,
|
||
fontSize: 15,
|
||
fontWeight: 700,
|
||
cursor: building ? "not-allowed" : "pointer",
|
||
letterSpacing: "-0.01em",
|
||
boxShadow: JM.primaryShadow,
|
||
opacity: building ? 0.75 : 1,
|
||
}}
|
||
>
|
||
{building ? "Starting…" : "Build my product"}
|
||
</button>
|
||
<p style={{ fontSize: 12.5, color: JM.muted, textAlign: "center", marginTop: 10, marginBottom: 4, opacity: 0.85 }}>
|
||
No code needed · You can edit everything after
|
||
</p>
|
||
</div>
|
||
|
||
<div style={{ textAlign: "center", paddingBottom: 8 }}>
|
||
<button
|
||
type="button"
|
||
onClick={() => router.push(`/${workspace}/project/${projectId}/design`)}
|
||
style={{
|
||
background: "none",
|
||
border: "none",
|
||
fontFamily: JM.fontSans,
|
||
fontSize: 12.5,
|
||
color: JM.muted,
|
||
cursor: "pointer",
|
||
padding: "6px 0",
|
||
}}
|
||
>
|
||
← Go back and tweak choices
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{(uiPhase === "progress" || uiPhase === "done") && (
|
||
<div style={{ padding: 32, maxWidth: 580, margin: "0 auto", width: "100%" }}>
|
||
<div style={{ textAlign: "center", marginBottom: 26 }}>
|
||
{uiPhase === "done" ? (
|
||
<>
|
||
<div style={{ fontSize: 36, marginBottom: 12 }}>🚀</div>
|
||
<div
|
||
className="justine-grad-title"
|
||
style={{ fontSize: 24, fontWeight: 700, letterSpacing: "-0.03em", marginBottom: 6, fontFamily: JM.fontDisplay }}
|
||
>
|
||
Your MVP is live
|
||
</div>
|
||
<div style={{ fontSize: 13.5, color: JM.muted }}>
|
||
Deployed to Coolify · Pushed to Gitea · Ready to share
|
||
</div>
|
||
</>
|
||
) : (
|
||
<>
|
||
<div
|
||
className="justine-grad-anim"
|
||
style={{ fontSize: 24, fontWeight: 700, letterSpacing: "-0.03em", marginBottom: 6, fontFamily: JM.fontDisplay }}
|
||
>
|
||
Building your product…
|
||
</div>
|
||
<div style={{ fontSize: 13.5, color: JM.muted }}>
|
||
Step {curStep} of {STEPS.length}
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
<div
|
||
style={{
|
||
background: "#fff",
|
||
border: `1px solid ${JM.border}`,
|
||
borderRadius: 13,
|
||
overflow: "hidden",
|
||
marginBottom: 18,
|
||
}}
|
||
>
|
||
{renderStepRows()}
|
||
</div>
|
||
{uiPhase === "done" && (
|
||
<div>
|
||
<div
|
||
style={{
|
||
background: "#fff",
|
||
border: `1px solid ${JM.border}`,
|
||
borderRadius: 12,
|
||
padding: "18px 20px",
|
||
marginBottom: 14,
|
||
}}
|
||
>
|
||
<div className="justine-grad-title" style={{ fontSize: 10.5, fontWeight: 700, textTransform: "uppercase", letterSpacing: "0.06em", marginBottom: 12 }}>
|
||
Your next 3 actions
|
||
</div>
|
||
{[
|
||
{
|
||
n: "1",
|
||
t: "Open your live app",
|
||
d: "Share the URL with 5 real people today.",
|
||
},
|
||
{
|
||
n: "2",
|
||
t: "Sign up as a user",
|
||
d: "Go through your own onboarding. Fix anything confusing.",
|
||
},
|
||
{
|
||
n: "3",
|
||
t: "Post your first topic",
|
||
d: "AI has drafted your first content batch. Publish one today.",
|
||
},
|
||
].map((a, i, arr) => (
|
||
<div
|
||
key={a.n}
|
||
style={{
|
||
display: "flex",
|
||
gap: 12,
|
||
padding: "10px 0",
|
||
borderBottom: i < arr.length - 1 ? `1px solid ${JM.border}` : undefined,
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
width: 24,
|
||
height: 24,
|
||
borderRadius: "50%",
|
||
background: JM.primaryGradient,
|
||
color: "#fff",
|
||
fontSize: 11,
|
||
fontWeight: 700,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
flexShrink: 0,
|
||
}}
|
||
>
|
||
{a.n}
|
||
</div>
|
||
<div>
|
||
<div style={{ fontSize: 13, fontWeight: 600, color: JM.ink, marginBottom: 2 }}>{a.t}</div>
|
||
<div style={{ fontSize: 12, color: JM.muted, lineHeight: 1.5 }}>{a.d}</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
<div style={{ display: "flex", gap: 10 }}>
|
||
<button
|
||
type="button"
|
||
onClick={onSwitchToPreview}
|
||
className="build-cta"
|
||
style={{
|
||
flex: 2,
|
||
background: JM.primaryGradient,
|
||
color: "#fff",
|
||
border: "none",
|
||
borderRadius: 11,
|
||
padding: "14px 20px",
|
||
fontFamily: JM.fontSans,
|
||
fontSize: 14,
|
||
fontWeight: 700,
|
||
cursor: "pointer",
|
||
boxShadow: JM.primaryShadow,
|
||
}}
|
||
>
|
||
Open my app ↗
|
||
</button>
|
||
<a
|
||
href={giteaHref}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
style={{
|
||
flex: 1,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
background: "transparent",
|
||
color: JM.mid,
|
||
border: `1px solid ${JM.border}`,
|
||
borderRadius: 11,
|
||
padding: "14px 16px",
|
||
fontFamily: JM.fontSans,
|
||
fontSize: 13,
|
||
fontWeight: 500,
|
||
textDecoration: "none",
|
||
}}
|
||
>
|
||
View in Gitea ↗
|
||
</a>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</>
|
||
);
|
||
}
|