feat: restructure project nav to Atlas | PRD | Build | Growth | Assist | Analytics
Tab bar: - Removed: Design, Launch, Grow, Insights, Settings tabs - Added: Growth, Assist, Analytics as top-level tabs - Build remains, now a full hub Build hub (/build): - Left sub-nav groups: Code (apps), Layouts (surfaces), Infrastructure (6 items) - Code section: scoped file browser per selected app - Layouts section: surface overview cards with Edit link to /design - Infrastructure section: summary panel linking to /infrastructure?tab= Growth (/growth): - Left nav: Marketing Site, Communications, Channels, Pages - Each section: description + feature item grid + feedback CTA Assist (/assist): - Left nav: Emails, Chat Support, Support Site, Communications - Each section: description + feature item grid + feedback CTA Analytics (/analytics): - Left nav: Customers, Usage, Events, Reports - Each section: description + feature item grid + feedback CTA Made-with: Cursor
This commit is contained in:
144
app/[workspace]/project/[projectId]/growth/page.tsx
Normal file
144
app/[workspace]/project/[projectId]/growth/page.tsx
Normal file
@@ -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 (
|
||||
<div style={{ display: "flex", height: "100%", fontFamily: "Outfit, sans-serif", overflow: "hidden" }}>
|
||||
|
||||
{/* Left nav */}
|
||||
<div style={{ width: 200, flexShrink: 0, borderRight: "1px solid #e8e4dc", background: "#faf8f5", display: "flex", flexDirection: "column", overflow: "auto" }}>
|
||||
<div style={NAV_GROUP}>Growth</div>
|
||||
{SECTIONS.map(s => {
|
||||
const isActive = activeId === s.id;
|
||||
return (
|
||||
<button key={s.id} onClick={() => setSection(s.id)} style={{
|
||||
display: "flex", alignItems: "center", gap: 8, width: "100%", textAlign: "left",
|
||||
background: isActive ? "#f0ece4" : "transparent", border: "none", cursor: "pointer",
|
||||
padding: "6px 12px", borderRadius: 5,
|
||||
fontSize: "0.78rem", fontWeight: isActive ? 600 : 440,
|
||||
color: isActive ? "#1a1a1a" : "#5a5550",
|
||||
}}
|
||||
onMouseEnter={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
|
||||
onMouseLeave={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
||||
>
|
||||
<span style={{ fontSize: "0.65rem", opacity: 0.55, width: 14, textAlign: "center" }}>{s.icon}</span>
|
||||
{s.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div style={{ flex: 1, overflow: "auto", display: "flex", flexDirection: "column" }}>
|
||||
<div style={{ padding: "28px 32px", maxWidth: 800 }}>
|
||||
<div style={{ marginBottom: 24 }}>
|
||||
<div style={{ fontSize: "1.1rem", fontWeight: 700, color: "#1a1a1a", marginBottom: 6 }}>{active.title}</div>
|
||||
<div style={{ fontSize: "0.82rem", color: "#6b6560", lineHeight: 1.65, maxWidth: 520 }}>{active.desc}</div>
|
||||
</div>
|
||||
|
||||
{/* Feature items */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))", gap: 12, marginBottom: 32 }}>
|
||||
{active.items.map(item => (
|
||||
<div key={item} style={{
|
||||
background: "#fff", border: "1px solid #e8e4dc", borderRadius: 9,
|
||||
padding: "14px 16px", display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||
}}>
|
||||
<span style={{ fontSize: "0.8rem", fontWeight: 500, color: "#1a1a1a" }}>{item}</span>
|
||||
<span style={{ fontSize: "0.65rem", color: "#c5c0b8", background: "#f6f4f0", padding: "2px 7px", borderRadius: 4 }}>Soon</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div style={{
|
||||
background: "linear-gradient(135deg, #1a1a1a 0%, #2d2820 100%)",
|
||||
borderRadius: 12, padding: "24px 28px",
|
||||
display: "flex", alignItems: "center", justifyContent: "space-between",
|
||||
gap: 20,
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ fontSize: "0.85rem", fontWeight: 600, color: "#fff", marginBottom: 4 }}>
|
||||
{active.title} is coming to VIBN
|
||||
</div>
|
||||
<div style={{ fontSize: "0.75rem", color: "#8a8478", lineHeight: 1.5 }}>
|
||||
We're building this section next. Shape it by telling us what you need.
|
||||
</div>
|
||||
</div>
|
||||
<button style={{
|
||||
background: "#d4a04a", color: "#fff", border: "none", borderRadius: 8,
|
||||
padding: "9px 20px", fontSize: "0.78rem", fontWeight: 600,
|
||||
cursor: "pointer", whiteSpace: "nowrap", flexShrink: 0,
|
||||
}}>
|
||||
Give feedback
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GrowthPage() {
|
||||
return (
|
||||
<Suspense fallback={<div style={{ display: "flex", height: "100%", alignItems: "center", justifyContent: "center", color: "#a09a90", fontFamily: "Outfit, sans-serif", fontSize: "0.85rem" }}>Loading…</div>}>
|
||||
<GrowthInner />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user