- {/* Line numbers */}
-
- {lines.map((_, i) => (
-
- {i + 1}
-
- ))}
+
+
+ {lines.map((_, i) =>
{i + 1}
)}
- {/* Code */}
-
+
{highlightCode(fileContent, lang)}
@@ -401,12 +320,100 @@ function BuildPageInner() {
);
}
-// ── Page export (Suspense wraps useSearchParams) ──────────────────────────────
+// ── Main Build hub ────────────────────────────────────────────────────────────
+
+function BuildHubInner() {
+ const params = useParams();
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const projectId = params.projectId as string;
+ const workspace = params.workspace as string;
+
+ const section = searchParams.get("section") ?? "code";
+ const activeApp = searchParams.get("app") ?? "";
+ const activeRoot = searchParams.get("root") ?? "";
+ const activeInfra = searchParams.get("tab") ?? "builds";
+ const activeSurfaceParam = searchParams.get("surface") ?? "";
+
+ const [apps, setApps] = useState
([]);
+ const [surfaces, setSurfaces] = useState([]);
+ const [activeSurfaceId, setActiveSurfaceId] = useState(activeSurfaceParam);
+
+ useEffect(() => {
+ fetch(`/api/projects/${projectId}/apps`).then(r => r.json()).then(d => setApps(d.apps ?? [])).catch(() => {});
+ fetch(`/api/projects/${projectId}/design-surfaces`).then(r => r.json()).then(d => {
+ const ids: string[] = d.surfaces ?? [];
+ const themes: Record = d.surfaceThemes ?? {};
+ setSurfaces(ids.map(id => ({ id, label: SURFACE_LABELS[id] ?? id, lockedTheme: themes[id] })));
+ if (!activeSurfaceId && ids.length > 0) setActiveSurfaceId(ids[0]);
+ }).catch(() => {});
+ }, [projectId]);
+
+ const navigate = (params: Record) => {
+ const sp = new URLSearchParams({ section, ...params });
+ router.push(`/${workspace}/project/${projectId}/build?${sp.toString()}`, { scroll: false });
+ };
+
+ const setSection = (s: string) => router.push(`/${workspace}/project/${projectId}/build?section=${s}`, { scroll: false });
+
+ return (
+
+
+ {/* ── Left nav ── */}
+
+
+ {/* Code group */}
+
Code
+ {apps.length > 0 ? apps.map(app => (
+
navigate({ section: "code", app: app.name, root: app.path })}
+ />
+ )) : (
+ setSection("code")} />
+ )}
+
+ {/* Layouts group */}
+ Layouts
+ {surfaces.length > 0 ? surfaces.map(s => (
+ { setActiveSurfaceId(s.id); navigate({ section: "layouts", surface: s.id }); }}
+ />
+ )) : (
+ setSection("layouts")} />
+ )}
+
+ {/* Infrastructure group */}
+ Infrastructure
+ {INFRA_ITEMS.map(item => (
+ navigate({ section: "infrastructure", tab: item.id })}
+ />
+ ))}
+
+
+ {/* ── Content ── */}
+
+ {section === "code" && (
+
+ )}
+ {section === "layouts" && (
+ { setActiveSurfaceId(id); navigate({ section: "layouts", surface: id }); }} />
+ )}
+ {section === "infrastructure" && (
+
+ )}
+
+
+ );
+}
export default function BuildPage() {
return (
Loading…}>
-
+
);
}
diff --git a/app/[workspace]/project/[projectId]/growth/page.tsx b/app/[workspace]/project/[projectId]/growth/page.tsx
new file mode 100644
index 0000000..6f80c74
--- /dev/null
+++ b/app/[workspace]/project/[projectId]/growth/page.tsx
@@ -0,0 +1,144 @@
+"use client";
+
+import { Suspense } from "react";
+import { useParams, useSearchParams, useRouter } from "next/navigation";
+
+const SECTIONS = [
+ {
+ id: "marketing-site",
+ label: "Marketing Site",
+ icon: "◌",
+ title: "Marketing Site",
+ desc: "Your public-facing website — hero, features, pricing, blog, and landing pages. Connected to your design surface and deployed via your infrastructure.",
+ items: ["Hero & Landing", "Features", "Pricing Page", "Blog", "Case Studies", "About"],
+ },
+ {
+ id: "communications",
+ label: "Communications",
+ icon: "◈",
+ title: "Communications",
+ desc: "Outbound messaging — product announcements, newsletters, launch emails, and drip campaigns sent to your audience.",
+ items: ["Announcements", "Newsletter", "Launch Sequence", "Drip Campaigns"],
+ },
+ {
+ id: "channels",
+ label: "Channels",
+ icon: "↗",
+ title: "Distribution Channels",
+ desc: "Where your product gets discovered — SEO, social, Product Hunt, app stores, partnerships, and paid acquisition.",
+ items: ["SEO & Search", "Social Media", "Product Hunt", "App Stores", "Partnerships", "Paid Ads"],
+ },
+ {
+ id: "pages",
+ label: "Pages",
+ icon: "▭",
+ title: "Pages",
+ desc: "Individual landing pages for campaigns, experiments, and specific audience segments. Build, publish, and A/B test.",
+ items: ["Campaign Pages", "A/B Tests", "Event Pages", "Partner Pages", "Waitlist"],
+ },
+] as const;
+
+type SectionId = typeof SECTIONS[number]["id"];
+
+const NAV_GROUP: React.CSSProperties = {
+ fontSize: "0.6rem", fontWeight: 700, color: "#b5b0a6",
+ letterSpacing: "0.09em", textTransform: "uppercase",
+ padding: "14px 12px 6px", fontFamily: "Outfit, sans-serif",
+};
+
+function GrowthInner() {
+ const params = useParams();
+ const searchParams = useSearchParams();
+ const router = useRouter();
+ const workspace = params.workspace as string;
+ const projectId = params.projectId as string;
+
+ const activeId = (searchParams.get("section") ?? "marketing-site") as SectionId;
+ const active = SECTIONS.find(s => s.id === activeId) ?? SECTIONS[0];
+
+ const setSection = (id: string) =>
+ router.push(`/${workspace}/project/${projectId}/growth?section=${id}`, { scroll: false });
+
+ return (
+
+
+ {/* Left nav */}
+
+
Growth
+ {SECTIONS.map(s => {
+ const isActive = activeId === s.id;
+ return (
+
+ );
+ })}
+
+
+ {/* Content */}
+
+
+
+
{active.title}
+
{active.desc}
+
+
+ {/* Feature items */}
+
+ {active.items.map(item => (
+
+ {item}
+ Soon
+
+ ))}
+
+
+ {/* CTA */}
+
+
+
+ {active.title} is coming to VIBN
+
+
+ We're building this section next. Shape it by telling us what you need.
+
+
+
+
+
+
+
+ );
+}
+
+export default function GrowthPage() {
+ return (
+
Loading…}>
+
+
+ );
+}
diff --git a/components/layout/project-shell.tsx b/components/layout/project-shell.tsx
index 8aa2a88..272617f 100644
--- a/components/layout/project-shell.tsx
+++ b/components/layout/project-shell.tsx
@@ -23,14 +23,12 @@ interface ProjectShellProps {
}
const ALL_TABS = [
- { id: "overview", label: "Atlas", path: "overview" },
- { id: "prd", label: "PRD", path: "prd" },
- { id: "design", label: "Design", path: "design" },
- { id: "build", label: "Build", path: "build" },
- { id: "deployment", label: "Launch", path: "deployment" },
- { id: "grow", label: "Grow", path: "grow" },
- { id: "insights", label: "Insights", path: "insights" },
- { id: "settings", label: "Settings", path: "settings" },
+ { id: "overview", label: "Atlas", path: "overview" },
+ { id: "prd", label: "PRD", path: "prd" },
+ { id: "build", label: "Build", path: "build" },
+ { id: "growth", label: "Growth", path: "growth" },
+ { id: "assist", label: "Assist", path: "assist" },
+ { id: "analytics", label: "Analytics", path: "analytics" },
];
function getTabsForMode(
@@ -41,7 +39,7 @@ function getTabsForMode(
return ALL_TABS.filter(t => t.id !== "prd");
case "migration":
return ALL_TABS
- .filter(t => !["prd", "grow", "insights"].includes(t.id))
+ .filter(t => t.id !== "prd")
.map(t => t.id === "overview" ? { ...t, label: "Migration Plan" } : t);
default:
return ALL_TABS;