A new home for everything that happens BEFORE building: - Vision — one-line elevator pitch (mirrors productVision) - Ideas — the "park-it" bin for raw thoughts - Tasks — what needs to happen next (open / done) - Decisions — log of "we chose X over Y because Z" Storage is appended under fs_projects.data.plan so no schema migration is needed. CRUD lives at /api/projects/[projectId]/plan. The bare project URL now redirects to /plan instead of /product, and the AI chat receives decisions + open tasks in its active-project context block — so it stops re-litigating settled questions and knows what's queued up. Made-with: Cursor
78 lines
2.3 KiB
TypeScript
78 lines
2.3 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Project tab bar — Product · Infrastructure · Hosting.
|
|
*
|
|
* Lives at the top of the cream main area, right below the project
|
|
* header. The active tab is determined by the URL pathname so back /
|
|
* forward / refresh always highlight the right one.
|
|
*/
|
|
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { Box, Cloud, Server, NotebookPen } from "lucide-react";
|
|
|
|
const TABS = [
|
|
{ id: "plan", label: "Plan", icon: NotebookPen, blurb: "Vision, ideas, tasks, and decisions for this project." },
|
|
{ id: "product", label: "Product", icon: Box, blurb: "Custom code, design, and content built for this vision." },
|
|
{ id: "infrastructure", label: "Infrastructure", icon: Server, blurb: "Swappable services this product depends on." },
|
|
{ id: "hosting", label: "Hosting", icon: Cloud, blurb: "Where it runs and how people reach it." },
|
|
] as const;
|
|
|
|
export function ProjectTabBar({
|
|
workspace,
|
|
projectId,
|
|
}: {
|
|
workspace: string;
|
|
projectId: string;
|
|
}) {
|
|
const pathname = usePathname() ?? "";
|
|
const activeTab =
|
|
TABS.find(t => pathname.includes(`/project/${projectId}/${t.id}`))?.id ??
|
|
"plan";
|
|
|
|
return (
|
|
<nav style={tabBar} aria-label="Project sections">
|
|
{TABS.map(tab => {
|
|
const isActive = tab.id === activeTab;
|
|
const Icon = tab.icon;
|
|
return (
|
|
<Link
|
|
key={tab.id}
|
|
href={`/${workspace}/project/${projectId}/${tab.id}`}
|
|
style={{
|
|
...tabLink,
|
|
color: isActive ? "#1a1a1a" : "#5f5e5a",
|
|
borderBottomColor: isActive ? "#1a1a1a" : "transparent",
|
|
fontWeight: isActive ? 600 : 500,
|
|
}}
|
|
title={tab.blurb}
|
|
>
|
|
<Icon size={14} style={{ opacity: isActive ? 1 : 0.7 }} />
|
|
{tab.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
const tabBar: React.CSSProperties = {
|
|
display: "flex",
|
|
gap: 4,
|
|
marginTop: 22,
|
|
marginBottom: -1,
|
|
};
|
|
|
|
const tabLink: React.CSSProperties = {
|
|
display: "inline-flex",
|
|
alignItems: "center",
|
|
gap: 8,
|
|
padding: "10px 14px",
|
|
fontSize: "0.82rem",
|
|
textDecoration: "none",
|
|
borderBottom: "2px solid transparent",
|
|
transition: "color 0.15s, border-color 0.15s",
|
|
fontFamily: '"Outfit", "Inter", ui-sans-serif, sans-serif',
|
|
};
|