Files
vibn-frontend/components/project-main/FreshIdeaMain.tsx
Mark Henderson bada63452f feat(ui): apply Justine ink & parchment design system
- Map Justine tokens to shadcn CSS variables (--vibn-* aliases)
- Switch fonts to Inter + Lora via next/font (IBM Plex Mono for code)
- Base typography: body Inter, h1–h3 Lora; marketing hero + wordmark serif
- Project shell and global chrome use semantic colors
- Replace Outfit/Newsreader references across TSX inline styles

Made-with: Cursor
2026-04-01 21:03:40 -07:00

275 lines
10 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import { AtlasChat } from "@/components/AtlasChat";
import { useRouter, useParams } from "next/navigation";
import Link from "next/link";
const DISCOVERY_PHASES = [
"big_picture",
"users_personas",
"features_scope",
"business_model",
"screens_data",
"risks_questions",
];
// Maps discovery phases → the PRD sections they populate
const PRD_SECTIONS: { label: string; phase: string | null }[] = [
{ label: "Executive Summary", phase: "big_picture" },
{ label: "Problem Statement", phase: "big_picture" },
{ label: "Vision & Success Metrics", phase: "big_picture" },
{ label: "Users & Personas", phase: "users_personas" },
{ label: "User Flows", phase: "users_personas" },
{ label: "Feature Requirements", phase: "features_scope" },
{ label: "Screen Specs", phase: "features_scope" },
{ label: "Business Model", phase: "business_model" },
{ label: "Integrations & Dependencies", phase: "screens_data" },
{ label: "Non-Functional Reqs", phase: "features_scope" },
{ label: "Risks & Mitigations", phase: "risks_questions" },
{ label: "Open Questions", phase: "risks_questions" },
];
interface FreshIdeaMainProps {
projectId: string;
projectName: string;
}
export function FreshIdeaMain({ projectId, projectName }: FreshIdeaMainProps) {
const router = useRouter();
const params = useParams();
const workspace = params?.workspace as string;
const [savedPhaseIds, setSavedPhaseIds] = useState<Set<string>>(new Set());
const [allDone, setAllDone] = useState(false);
const [prdLoading, setPrdLoading] = useState(false);
const [dismissed, setDismissed] = useState(false);
const [hasPrd, setHasPrd] = useState(false);
useEffect(() => {
// Check if PRD already exists on the project
fetch(`/api/projects/${projectId}`)
.then(r => r.json())
.then(d => { if (d.project?.prd) setHasPrd(true); })
.catch(() => {});
const poll = () => {
fetch(`/api/projects/${projectId}/save-phase`)
.then(r => r.json())
.then(d => {
const ids = new Set<string>((d.phases ?? []).map((p: { phase: string }) => p.phase));
setSavedPhaseIds(ids);
const done = DISCOVERY_PHASES.every(id => ids.has(id));
setAllDone(done);
})
.catch(() => {});
};
poll();
const interval = setInterval(poll, 8_000);
return () => clearInterval(interval);
}, [projectId]);
const handleGeneratePRD = async () => {
if (prdLoading) return;
setPrdLoading(true);
try {
router.push(`/${workspace}/project/${projectId}/prd`);
} finally {
setPrdLoading(false);
}
};
const handleMVP = () => {
router.push(`/${workspace}/project/${projectId}/build`);
};
// PRD exists — show a thin notice bar at the top, then keep the chat fully accessible
const completedSections = PRD_SECTIONS.filter(({ phase }) =>
phase === null ? allDone : savedPhaseIds.has(phase)
).length;
const totalSections = PRD_SECTIONS.length;
return (
<div style={{ height: "100%", display: "flex", flexDirection: "row", overflow: "hidden" }}>
{/* ── Left: Atlas chat ── */}
<div style={{ flex: 1, display: "flex", flexDirection: "column", overflow: "hidden", minWidth: 0 }}>
{/* PRD ready notice — replaces the decision banner once PRD is saved */}
{hasPrd && (
<div style={{
background: "#1a1a1a", padding: "10px 20px",
display: "flex", alignItems: "center", justifyContent: "space-between",
gap: 16, flexShrink: 0, borderBottom: "1px solid #333",
}}>
<div style={{ fontSize: "0.8rem", color: "#e8e4dc", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif" }}>
PRD saved you can keep refining here or view the full document.
</div>
<Link
href={`/${workspace}/project/${projectId}/prd`}
style={{
padding: "6px 14px", borderRadius: 7,
background: "#fff", color: "#1a1a1a",
fontSize: "0.76rem", fontWeight: 600,
textDecoration: "none", flexShrink: 0,
}}
>
View PRD
</Link>
</div>
)}
{/* Decision banner — shown when all 6 phases are saved but PRD not yet generated */}
{allDone && !dismissed && !hasPrd && (
<div style={{
background: "linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%)",
padding: "14px 20px",
display: "flex", alignItems: "center", justifyContent: "space-between",
gap: 16, flexShrink: 0, flexWrap: "wrap",
borderBottom: "1px solid #333",
}}>
<div>
<div style={{ fontSize: "0.84rem", fontWeight: 700, color: "#fff", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", marginBottom: 2 }}>
Discovery complete what&apos;s next?
</div>
<div style={{ fontSize: "0.72rem", color: "#a09a90", fontFamily: "var(--font-inter), ui-sans-serif, sans-serif" }}>
All 6 phases captured. Generate your PRD or jump into Build.
</div>
</div>
<div style={{ display: "flex", gap: 8, flexShrink: 0 }}>
<button
onClick={handleGeneratePRD}
disabled={prdLoading}
style={{
padding: "8px 16px", borderRadius: 7, border: "none",
background: "#fff", color: "#1a1a1a",
fontSize: "0.8rem", fontWeight: 700,
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", cursor: "pointer",
transition: "opacity 0.12s",
}}
onMouseEnter={e => (e.currentTarget.style.opacity = "0.88")}
onMouseLeave={e => (e.currentTarget.style.opacity = "1")}
>
{prdLoading ? "Navigating…" : "Generate PRD →"}
</button>
<button
onClick={handleMVP}
style={{
padding: "8px 16px", borderRadius: 7,
border: "1px solid rgba(255,255,255,0.2)",
background: "transparent", color: "#fff",
fontSize: "0.8rem", fontWeight: 600,
fontFamily: "var(--font-inter), ui-sans-serif, sans-serif", cursor: "pointer",
}}
>
Plan MVP
</button>
<button
onClick={() => setDismissed(true)}
style={{
background: "none", border: "none", cursor: "pointer",
color: "#888", fontSize: "1rem", padding: "4px 6px",
}}
title="Dismiss"
>×</button>
</div>
</div>
)}
<AtlasChat projectId={projectId} projectName={projectName} />
</div>
{/* ── Right: PRD section tracker ── */}
<div style={{
width: 240, flexShrink: 0,
background: "#faf8f5",
borderLeft: "1px solid #e8e4dc",
display: "flex", flexDirection: "column",
overflow: "hidden",
}}>
{/* Header */}
<div style={{
padding: "14px 16px 10px",
borderBottom: "1px solid #e8e4dc",
flexShrink: 0,
}}>
<div style={{ fontSize: "0.72rem", fontWeight: 700, color: "#1a1a1a", letterSpacing: "0.06em", textTransform: "uppercase", marginBottom: 6 }}>
PRD Sections
</div>
{/* Progress bar */}
<div style={{ height: 3, background: "#e8e4dc", borderRadius: 99, overflow: "hidden" }}>
<div style={{
height: "100%", borderRadius: 99,
background: "#1a1a1a",
width: `${Math.round((completedSections / totalSections) * 100)}%`,
transition: "width 0.4s ease",
}} />
</div>
<div style={{ fontSize: "0.68rem", color: "#a09a90", marginTop: 5 }}>
{completedSections} of {totalSections} sections complete
</div>
</div>
{/* Section list */}
<div style={{ flex: 1, overflowY: "auto", padding: "8px 0" }}>
{PRD_SECTIONS.map(({ label, phase }) => {
const isDone = phase === null
? allDone // non-functional reqs generated when all done
: savedPhaseIds.has(phase);
return (
<div
key={label}
style={{
padding: "8px 16px",
display: "flex", alignItems: "flex-start", gap: 10,
}}
>
{/* Status dot */}
<div style={{
width: 8, height: 8, borderRadius: "50%", flexShrink: 0, marginTop: 4,
background: isDone ? "#1a1a1a" : "transparent",
border: isDone ? "none" : "1.5px solid #c8c4bc",
transition: "all 0.3s",
}} />
<div style={{ minWidth: 0 }}>
<div style={{
fontSize: "0.78rem", fontWeight: isDone ? 600 : 400,
color: isDone ? "#1a1a1a" : "#6b6560",
lineHeight: 1.3,
}}>
{label}
</div>
{!isDone && (
<div style={{ fontSize: "0.65rem", color: "#a09a90", marginTop: 2, lineHeight: 1.3 }}>
Complete this phase in Vibn
</div>
)}
</div>
</div>
);
})}
</div>
{/* Footer CTA */}
{allDone && (
<div style={{ padding: "12px 16px", borderTop: "1px solid #e8e4dc", flexShrink: 0 }}>
<Link
href={`/${workspace}/project/${projectId}/prd`}
style={{
display: "block", textAlign: "center",
padding: "9px 0", borderRadius: 7,
background: "#1a1a1a", color: "#fff",
fontSize: "0.78rem", fontWeight: 600,
textDecoration: "none",
}}
>
Generate PRD
</Link>
</div>
)}
</div>
</div>
);
}