- Add Google Fonts (Newsreader/Outfit/IBM Plex Mono) + warm beige CSS palette - New VIBNSidebar: Stackless-style 220px sidebar with project list + user footer - New ProjectShell: project header with name/status/progress% + tab bar - Tabs: Atlas → PRD → Design → Build → Deploy → Settings - New /prd page: section-by-section progress view - New /build page: locked until PRD complete - Projects list page: Stackless-style row layout - Simplify overview page to just render AtlasChat Made-with: Cursor
223 lines
6.6 KiB
TypeScript
223 lines
6.6 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { usePathname } from "next/navigation";
|
|
import { signOut, useSession } from "next-auth/react";
|
|
|
|
interface Project {
|
|
id: string;
|
|
productName: string;
|
|
status?: string;
|
|
}
|
|
|
|
interface VIBNSidebarProps {
|
|
workspace: string;
|
|
}
|
|
|
|
function StatusDot({ status }: { status?: string }) {
|
|
const color =
|
|
status === "live" ? "#2e7d32"
|
|
: status === "building" ? "#3d5afe"
|
|
: "#d4a04a";
|
|
const anim = status === "building" ? "vibn-breathe 2.5s ease infinite" : "none";
|
|
return (
|
|
<span
|
|
style={{
|
|
width: 7, height: 7, borderRadius: "50%",
|
|
background: color, display: "inline-block",
|
|
flexShrink: 0, animation: anim,
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function VIBNSidebar({ workspace }: VIBNSidebarProps) {
|
|
const pathname = usePathname();
|
|
const { data: session } = useSession();
|
|
const [projects, setProjects] = useState<Project[]>([]);
|
|
|
|
useEffect(() => {
|
|
fetch("/api/projects")
|
|
.then((r) => r.json())
|
|
.then((d) => setProjects(d.projects ?? []))
|
|
.catch(() => {});
|
|
}, []);
|
|
|
|
// Derive active project from URL
|
|
const activeProjectId = pathname?.match(/\/project\/([^/]+)/)?.[1] ?? null;
|
|
|
|
// Derive active top-level section
|
|
const isProjects = !activeProjectId && (pathname?.includes("/projects") || pathname?.includes("/project"));
|
|
const isActivity = pathname?.includes("/activity");
|
|
const isSettings = pathname?.includes("/settings");
|
|
|
|
const topNavItems = [
|
|
{ id: "projects", label: "Projects", icon: "⌗", href: `/${workspace}/projects` },
|
|
{ id: "settings", label: "Settings", icon: "⚙", href: `/${workspace}/settings` },
|
|
];
|
|
|
|
const userInitial = session?.user?.name?.[0]?.toUpperCase()
|
|
?? session?.user?.email?.[0]?.toUpperCase()
|
|
?? "?";
|
|
|
|
return (
|
|
<nav
|
|
style={{
|
|
width: 220,
|
|
height: "100vh",
|
|
background: "#fff",
|
|
borderRight: "1px solid #e8e4dc",
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
fontFamily: "Outfit, sans-serif",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
{/* Logo */}
|
|
<Link
|
|
href={`/${workspace}/projects`}
|
|
style={{
|
|
padding: "22px 18px 18px",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 9,
|
|
textDecoration: "none",
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 28, height: 28, borderRadius: 7, overflow: "hidden",
|
|
flexShrink: 0,
|
|
}}
|
|
>
|
|
<img src="/vibn-black-circle-logo.png" alt="VIBN" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
|
</div>
|
|
<span
|
|
style={{
|
|
fontSize: "0.95rem", fontWeight: 600, color: "#1a1a1a",
|
|
letterSpacing: "-0.03em", fontFamily: "Newsreader, serif",
|
|
}}
|
|
>
|
|
vibn
|
|
</span>
|
|
</Link>
|
|
|
|
{/* Top nav */}
|
|
<div style={{ padding: "4px 10px" }}>
|
|
{topNavItems.map((n) => {
|
|
const isActive = n.id === "projects" ? isProjects && !activeProjectId
|
|
: n.id === "settings" ? isSettings
|
|
: false;
|
|
return (
|
|
<Link
|
|
key={n.id}
|
|
href={n.href}
|
|
style={{
|
|
width: "100%",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 9,
|
|
padding: "8px 10px",
|
|
borderRadius: 6,
|
|
background: isActive ? "#f6f4f0" : "transparent",
|
|
color: isActive ? "#1a1a1a" : "#6b6560",
|
|
fontSize: "0.82rem",
|
|
fontWeight: isActive ? 600 : 500,
|
|
transition: "all 0.12s",
|
|
textDecoration: "none",
|
|
}}
|
|
>
|
|
<span style={{ fontSize: "0.8rem", opacity: 0.45, width: 18, textAlign: "center" }}>
|
|
{n.icon}
|
|
</span>
|
|
{n.label}
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
<div style={{ height: 1, background: "#eae6de", margin: "10px 18px" }} />
|
|
|
|
{/* Projects list */}
|
|
<div style={{ padding: "2px 10px", flex: 1, overflow: "auto" }}>
|
|
<div
|
|
style={{
|
|
fontSize: "0.6rem", fontWeight: 600, color: "#a09a90",
|
|
letterSpacing: "0.1em", textTransform: "uppercase",
|
|
padding: "6px 10px 8px",
|
|
}}
|
|
>
|
|
Projects
|
|
</div>
|
|
{projects.map((p) => {
|
|
const isActive = activeProjectId === p.id;
|
|
return (
|
|
<Link
|
|
key={p.id}
|
|
href={`/${workspace}/project/${p.id}/overview`}
|
|
style={{
|
|
width: "100%",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 9,
|
|
padding: "7px 10px",
|
|
borderRadius: 6,
|
|
background: isActive ? "#f6f4f0" : "transparent",
|
|
color: "#1a1a1a",
|
|
fontSize: "0.82rem",
|
|
fontWeight: isActive ? 600 : 450,
|
|
transition: "background 0.12s",
|
|
textDecoration: "none",
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
<StatusDot status={p.status} />
|
|
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
{p.productName}
|
|
</span>
|
|
</Link>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* User footer */}
|
|
<div
|
|
style={{
|
|
padding: "14px 18px",
|
|
borderTop: "1px solid #eae6de",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
gap: 9,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
width: 28, height: 28, borderRadius: "50%",
|
|
background: "#f0ece4", display: "flex", alignItems: "center",
|
|
justifyContent: "center", fontSize: "0.72rem", fontWeight: 600,
|
|
color: "#8a8478", flexShrink: 0,
|
|
}}
|
|
>
|
|
{userInitial}
|
|
</div>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<div style={{ fontSize: "0.78rem", fontWeight: 500, color: "#1a1a1a", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
|
{session?.user?.name ?? session?.user?.email?.split("@")[0] ?? "Account"}
|
|
</div>
|
|
<button
|
|
onClick={() => signOut({ callbackUrl: "/auth" })}
|
|
style={{
|
|
background: "none", border: "none", padding: 0,
|
|
fontSize: "0.62rem", color: "#a09a90", cursor: "pointer",
|
|
fontFamily: "Outfit, sans-serif",
|
|
}}
|
|
>
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|