refactor: rework project page layout - sidebar as product OS, full-width content
- VIBNSidebar: when inside a project, lower section now shows 7 product layer sections (Apps, Layouts, Infrastructure, Growth, Monetize, Support, Analytics) instead of the projects list. Sections self-fetch data from /api/projects/[id] and /api/projects/[id]/apps. On non-project pages, reverts to the projects list as before. - ProjectShell: removed the project header strip (name/status/progress bar) and the persistent 230px right panel entirely. Tab bar now sits at the top of the content area with no header above it. Content is full-width. Each page manages its own internal layout. Made-with: Cursor
This commit is contained in:
@@ -5,43 +5,138 @@ 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";
|
||||
interface ProjectData {
|
||||
id: string;
|
||||
productName?: string;
|
||||
name?: string;
|
||||
status?: string;
|
||||
giteaRepo?: string;
|
||||
giteaRepoUrl?: string;
|
||||
surfaces?: string[];
|
||||
surfaceThemes?: Record<string, string>;
|
||||
apps?: Array<{ name: string; path: string; coolifyServiceUuid?: string | null; domain?: string | null }>;
|
||||
}
|
||||
|
||||
interface AppEntry {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
// ── Section helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
function SectionHeading({ label, collapsed }: { label: string; collapsed: boolean }) {
|
||||
if (collapsed) return null;
|
||||
return (
|
||||
<span style={{
|
||||
width: 7, height: 7, borderRadius: "50%",
|
||||
background: color, display: "inline-block",
|
||||
flexShrink: 0, animation: anim,
|
||||
}} />
|
||||
<div style={{
|
||||
fontSize: "0.58rem", fontWeight: 700, color: "#b5b0a6",
|
||||
letterSpacing: "0.1em", textTransform: "uppercase",
|
||||
padding: "14px 12px 5px",
|
||||
}}>
|
||||
{label}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionRow({
|
||||
icon, label, href, dim, collapsed,
|
||||
}: {
|
||||
icon: string;
|
||||
label: string;
|
||||
href?: string;
|
||||
dim?: boolean;
|
||||
collapsed: boolean;
|
||||
}) {
|
||||
const style: React.CSSProperties = {
|
||||
display: "flex", alignItems: "center",
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
gap: 8, padding: collapsed ? "7px 0" : "5px 12px",
|
||||
borderRadius: 5, textDecoration: "none",
|
||||
color: dim ? "#c5c0b8" : "#4a4640",
|
||||
fontSize: "0.78rem", fontWeight: 450,
|
||||
transition: "background 0.1s",
|
||||
width: "100%", boxSizing: "border-box" as const,
|
||||
};
|
||||
|
||||
const inner = (
|
||||
<>
|
||||
<span style={{ fontSize: "0.72rem", opacity: 0.6, flexShrink: 0, width: collapsed ? "auto" : 14, textAlign: "center" }}>
|
||||
{icon}
|
||||
</span>
|
||||
{!collapsed && (
|
||||
<span style={{
|
||||
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
|
||||
fontStyle: dim ? "italic" : "normal",
|
||||
}}>
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<Link href={href} title={collapsed ? label : undefined} style={style}
|
||||
onMouseEnter={e => { if (!dim) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
|
||||
onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
||||
>
|
||||
{inner}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div title={collapsed ? label : undefined} style={{ ...style, cursor: "default" }}>
|
||||
{inner}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionDivider() {
|
||||
return <div style={{ height: 1, background: "#eae6de", margin: "8px 12px" }} />;
|
||||
}
|
||||
|
||||
// ── Surface label map ────────────────────────────────────────────────────────
|
||||
const SURFACE_LABELS: Record<string, string> = {
|
||||
"marketing": "Marketing site",
|
||||
"web-app": "Web app",
|
||||
"admin": "Admin panel",
|
||||
"api": "API layer",
|
||||
};
|
||||
|
||||
const SURFACE_ICONS: Record<string, string> = {
|
||||
"marketing": "◎",
|
||||
"web-app": "⬡",
|
||||
"admin": "◫",
|
||||
"api": "⌁",
|
||||
};
|
||||
|
||||
// ── Main sidebar ─────────────────────────────────────────────────────────────
|
||||
|
||||
const COLLAPSED_KEY = "vibn_sidebar_collapsed";
|
||||
const COLLAPSED_W = 56;
|
||||
const EXPANDED_W = 220;
|
||||
const COLLAPSED_W = 52;
|
||||
const EXPANDED_W = 216;
|
||||
|
||||
export function VIBNSidebar({ workspace }: VIBNSidebarProps) {
|
||||
const pathname = usePathname();
|
||||
const { data: session } = useSession();
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// Restore collapse state from localStorage
|
||||
// Project-specific data
|
||||
const [project, setProject] = useState<ProjectData | null>(null);
|
||||
const [apps, setApps] = useState<AppEntry[]>([]);
|
||||
|
||||
// Global projects list (used when NOT inside a project)
|
||||
const [projects, setProjects] = useState<Array<{ id: string; productName: string; status?: string }>>([]);
|
||||
|
||||
const activeProjectId = pathname?.match(/\/project\/([^/]+)/)?.[1] ?? null;
|
||||
const activeTab = pathname?.match(/\/project\/[^/]+\/([^/]+)/)?.[1] ?? null;
|
||||
|
||||
// Restore collapse state
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem(COLLAPSED_KEY);
|
||||
if (stored === "1") setCollapsed(true);
|
||||
@@ -55,14 +150,30 @@ export function VIBNSidebar({ workspace }: VIBNSidebarProps) {
|
||||
});
|
||||
};
|
||||
|
||||
// Fetch global projects list (for non-project pages)
|
||||
useEffect(() => {
|
||||
if (activeProjectId) return;
|
||||
fetch("/api/projects")
|
||||
.then((r) => r.json())
|
||||
.then((d) => setProjects(d.projects ?? []))
|
||||
.then(r => r.json())
|
||||
.then(d => setProjects(d.projects ?? []))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
}, [activeProjectId]);
|
||||
|
||||
// Fetch project-specific data when inside a project
|
||||
useEffect(() => {
|
||||
if (!activeProjectId) { setProject(null); setApps([]); return; }
|
||||
|
||||
fetch(`/api/projects/${activeProjectId}`)
|
||||
.then(r => r.json())
|
||||
.then(d => setProject(d.project ?? null))
|
||||
.catch(() => {});
|
||||
|
||||
fetch(`/api/projects/${activeProjectId}/apps`)
|
||||
.then(r => r.json())
|
||||
.then(d => setApps(d.apps ?? []))
|
||||
.catch(() => {});
|
||||
}, [activeProjectId]);
|
||||
|
||||
const activeProjectId = pathname?.match(/\/project\/([^/]+)/)?.[1] ?? null;
|
||||
const isProjects = !activeProjectId && (pathname?.includes("/projects") || pathname?.includes("/project"));
|
||||
const isActivity = !activeProjectId && pathname?.includes("/activity");
|
||||
const isSettings = !activeProjectId && pathname?.includes("/settings");
|
||||
@@ -78,117 +189,90 @@ export function VIBNSidebar({ workspace }: VIBNSidebarProps) {
|
||||
?? "?";
|
||||
|
||||
const w = collapsed ? COLLAPSED_W : EXPANDED_W;
|
||||
|
||||
// Don't animate on initial mount (avoid flash)
|
||||
const transition = mounted ? "width 0.2s cubic-bezier(0.4,0,0.2,1)" : "none";
|
||||
|
||||
const base = `/${workspace}/project/${activeProjectId}`;
|
||||
|
||||
// Surfaces locked in on design page
|
||||
const surfaces = project?.surfaces ?? [];
|
||||
// Coolify/monorepo apps
|
||||
const infraApps = project?.apps ?? [];
|
||||
|
||||
return (
|
||||
<nav style={{
|
||||
width: w,
|
||||
height: "100vh",
|
||||
background: "#fff",
|
||||
borderRight: "1px solid #e8e4dc",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
width: w, height: "100vh",
|
||||
background: "#fff", borderRight: "1px solid #e8e4dc",
|
||||
display: "flex", flexDirection: "column",
|
||||
fontFamily: "Outfit, sans-serif",
|
||||
flexShrink: 0,
|
||||
overflow: "hidden",
|
||||
transition,
|
||||
position: "relative",
|
||||
flexShrink: 0, overflow: "hidden",
|
||||
transition, position: "relative",
|
||||
}}>
|
||||
|
||||
{/* Logo + toggle row */}
|
||||
{/* ── Logo + toggle ── */}
|
||||
{collapsed ? (
|
||||
/* Collapsed: logo centered, toggle below it */
|
||||
<div style={{ flexShrink: 0 }}>
|
||||
<div style={{ display: "flex", justifyContent: "center", padding: "16px 0 8px" }}>
|
||||
<div style={{ display: "flex", justifyContent: "center", padding: "14px 0 6px" }}>
|
||||
<Link href={`/${workspace}/projects`} title="VIBN" style={{ textDecoration: "none" }}>
|
||||
<div style={{ width: 28, height: 28, borderRadius: 7, overflow: "hidden" }}>
|
||||
<div style={{ width: 26, height: 26, borderRadius: 7, overflow: "hidden" }}>
|
||||
<img src="/vibn-black-circle-logo.png" alt="VIBN" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
<div style={{ display: "flex", justifyContent: "center", paddingBottom: 8 }}>
|
||||
<button
|
||||
onClick={toggle}
|
||||
title="Expand sidebar"
|
||||
style={{
|
||||
background: "#f0ece4", border: "none", cursor: "pointer",
|
||||
color: "#6b6560", width: 28, height: 22, borderRadius: 5,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: "0.82rem", fontWeight: 700, transition: "background 0.12s, color 0.12s",
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget.style.background = "#e0dcd4"); (e.currentTarget.style.color = "#1a1a1a"); }}
|
||||
onMouseLeave={e => { (e.currentTarget.style.background = "#f0ece4"); (e.currentTarget.style.color = "#6b6560"); }}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
<button onClick={toggle} title="Expand sidebar" style={{
|
||||
background: "#f0ece4", border: "none", cursor: "pointer",
|
||||
color: "#6b6560", width: 26, height: 20, borderRadius: 5,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: "0.8rem", fontWeight: 700,
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget.style.background = "#e0dcd4"); }}
|
||||
onMouseLeave={e => { (e.currentTarget.style.background = "#f0ece4"); }}
|
||||
>›</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Expanded: logo + name on left, toggle on right */
|
||||
<div style={{ padding: "16px 12px 16px 18px", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 9, flexShrink: 0 }}>
|
||||
<div style={{ padding: "14px 10px 14px 16px", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 9, flexShrink: 0 }}>
|
||||
<Link href={`/${workspace}/projects`} style={{ display: "flex", alignItems: "center", gap: 9, textDecoration: "none", minWidth: 0 }}>
|
||||
<div style={{ width: 28, height: 28, borderRadius: 7, overflow: "hidden", flexShrink: 0 }}>
|
||||
<div style={{ width: 26, height: 26, 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", whiteSpace: "nowrap" }}>
|
||||
<span style={{ fontSize: "0.92rem", fontWeight: 600, color: "#1a1a1a", letterSpacing: "-0.03em", fontFamily: "Newsreader, serif", whiteSpace: "nowrap" }}>
|
||||
vibn
|
||||
</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={toggle}
|
||||
title="Collapse sidebar"
|
||||
style={{
|
||||
background: "#f0ece4", border: "none", cursor: "pointer",
|
||||
color: "#6b6560", width: 26, height: 24, borderRadius: 5,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: "0.82rem", fontWeight: 700, flexShrink: 0,
|
||||
transition: "background 0.12s, color 0.12s",
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget.style.background = "#e0dcd4"); (e.currentTarget.style.color = "#1a1a1a"); }}
|
||||
onMouseLeave={e => { (e.currentTarget.style.background = "#f0ece4"); (e.currentTarget.style.color = "#6b6560"); }}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<button onClick={toggle} title="Collapse sidebar" style={{
|
||||
background: "#f0ece4", border: "none", cursor: "pointer",
|
||||
color: "#6b6560", width: 24, height: 22, borderRadius: 5,
|
||||
display: "flex", alignItems: "center", justifyContent: "center",
|
||||
fontSize: "0.8rem", fontWeight: 700, flexShrink: 0,
|
||||
}}
|
||||
onMouseEnter={e => { (e.currentTarget.style.background = "#e0dcd4"); }}
|
||||
onMouseLeave={e => { (e.currentTarget.style.background = "#f0ece4"); }}
|
||||
>‹</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Top nav */}
|
||||
<div style={{ padding: collapsed ? "4px 8px" : "4px 10px", flexShrink: 0 }}>
|
||||
{topNavItems.map((n) => {
|
||||
{/* ── Top nav ── */}
|
||||
<div style={{ padding: collapsed ? "2px 6px" : "2px 8px", flexShrink: 0 }}>
|
||||
{topNavItems.map(n => {
|
||||
const isActive = n.id === "projects" ? isProjects
|
||||
: n.id === "activity" ? isActivity
|
||||
: n.id === "settings" ? isSettings
|
||||
: false;
|
||||
: isSettings;
|
||||
return (
|
||||
<Link
|
||||
key={n.id}
|
||||
href={n.href}
|
||||
title={collapsed ? n.label : undefined}
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
gap: 9,
|
||||
padding: collapsed ? "9px 0" : "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",
|
||||
}}
|
||||
<Link key={n.id} href={n.href} title={collapsed ? n.label : undefined} style={{
|
||||
width: "100%", display: "flex", alignItems: "center",
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
gap: 8, padding: collapsed ? "8px 0" : "7px 10px",
|
||||
borderRadius: 6,
|
||||
background: isActive ? "#f6f4f0" : "transparent",
|
||||
color: isActive ? "#1a1a1a" : "#6b6560",
|
||||
fontSize: "0.8rem", fontWeight: isActive ? 600 : 500,
|
||||
transition: "background 0.12s", textDecoration: "none",
|
||||
}}
|
||||
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: collapsed ? "1rem" : "0.8rem",
|
||||
opacity: collapsed ? (isActive ? 0.9 : 0.45) : 0.45,
|
||||
width: collapsed ? "auto" : 18,
|
||||
textAlign: "center",
|
||||
transition: "font-size 0.15s",
|
||||
}}>
|
||||
<span style={{ fontSize: collapsed ? "0.95rem" : "0.78rem", opacity: collapsed ? (isActive ? 0.9 : 0.45) : 0.45, width: collapsed ? "auto" : 16, textAlign: "center" }}>
|
||||
{n.icon}
|
||||
</span>
|
||||
{!collapsed && n.label}
|
||||
@@ -197,91 +281,173 @@ export function VIBNSidebar({ workspace }: VIBNSidebarProps) {
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ height: 1, background: "#eae6de", margin: "10px 18px", flexShrink: 0 }} />
|
||||
<div style={{ height: 1, background: "#eae6de", margin: "8px 14px", flexShrink: 0 }} />
|
||||
|
||||
{/* Projects list */}
|
||||
<div style={{ padding: collapsed ? "2px 8px" : "2px 10px", flex: 1, overflow: "auto" }}>
|
||||
{!collapsed && (
|
||||
<div style={{
|
||||
fontSize: "0.6rem", fontWeight: 600, color: "#a09a90",
|
||||
letterSpacing: "0.1em", textTransform: "uppercase",
|
||||
padding: "6px 10px 8px",
|
||||
}}>
|
||||
Projects
|
||||
{/* ── Lower section ── */}
|
||||
<div style={{ flex: 1, overflow: "auto", paddingBottom: 8 }}>
|
||||
|
||||
{activeProjectId && project ? (
|
||||
/* ── PROJECT VIEW: 7 product layer sections ── */
|
||||
<>
|
||||
{/* Project name */}
|
||||
{!collapsed && (
|
||||
<div style={{ padding: "4px 12px 10px" }}>
|
||||
<div style={{ fontSize: "0.82rem", fontWeight: 700, color: "#1a1a1a", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{project.productName || project.name || "Project"}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.68rem", color: "#a09a90", marginTop: 1 }}>
|
||||
{project.status === "live" ? "● Live"
|
||||
: project.status === "building" ? "● Building"
|
||||
: "● Defining"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Apps ── */}
|
||||
<SectionHeading label="Apps" collapsed={collapsed} />
|
||||
{apps.length > 0 ? (
|
||||
apps.map(app => (
|
||||
<SectionRow
|
||||
key={app.name}
|
||||
icon="▢"
|
||||
label={app.name}
|
||||
href={project.giteaRepoUrl ? `${project.giteaRepoUrl}/src/branch/main/${app.path}` : undefined}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<SectionRow icon="▢" label="No apps yet" dim collapsed={collapsed} />
|
||||
)}
|
||||
|
||||
<SectionDivider />
|
||||
|
||||
{/* ── Layouts ── */}
|
||||
<SectionHeading label="Layouts" collapsed={collapsed} />
|
||||
{surfaces.length > 0 ? (
|
||||
surfaces.map(s => (
|
||||
<SectionRow
|
||||
key={s}
|
||||
icon={SURFACE_ICONS[s] ?? "◌"}
|
||||
label={SURFACE_LABELS[s] ?? s}
|
||||
href={`${base}/design`}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<SectionRow icon="◌" label="Not configured" dim collapsed={collapsed} />
|
||||
)}
|
||||
|
||||
<SectionDivider />
|
||||
|
||||
{/* ── Infrastructure ── */}
|
||||
<SectionHeading label="Infrastructure" collapsed={collapsed} />
|
||||
{infraApps.length > 0 ? (
|
||||
infraApps.map(app => (
|
||||
<SectionRow
|
||||
key={app.name}
|
||||
icon="◈"
|
||||
label={app.domain ?? app.name}
|
||||
href={app.domain ? `https://${app.domain}` : undefined}
|
||||
collapsed={collapsed}
|
||||
/>
|
||||
))
|
||||
) : project?.giteaRepo ? (
|
||||
<SectionRow icon="◈" label="Gitea connected" href={project.giteaRepoUrl} collapsed={collapsed} />
|
||||
) : (
|
||||
<SectionRow icon="◈" label="Not deployed" dim collapsed={collapsed} />
|
||||
)}
|
||||
|
||||
<SectionDivider />
|
||||
|
||||
{/* ── Growth ── */}
|
||||
<SectionHeading label="Growth" collapsed={collapsed} />
|
||||
<SectionRow icon="↗" label="Not set up" dim href={`${base}/grow`} collapsed={collapsed} />
|
||||
|
||||
<SectionDivider />
|
||||
|
||||
{/* ── Monetize ── */}
|
||||
<SectionHeading label="Monetize" collapsed={collapsed} />
|
||||
<SectionRow icon="◉" label="Not set up" dim collapsed={collapsed} />
|
||||
|
||||
<SectionDivider />
|
||||
|
||||
{/* ── Support ── */}
|
||||
<SectionHeading label="Support" collapsed={collapsed} />
|
||||
<SectionRow icon="?" label="Not set up" dim collapsed={collapsed} />
|
||||
|
||||
<SectionDivider />
|
||||
|
||||
{/* ── Analytics ── */}
|
||||
<SectionHeading label="Analytics" collapsed={collapsed} />
|
||||
<SectionRow icon="∿" label="Not set up" dim href={`${base}/insights`} collapsed={collapsed} />
|
||||
</>
|
||||
) : (
|
||||
/* ── GLOBAL VIEW: projects list ── */
|
||||
<div style={{ padding: collapsed ? "2px 6px" : "2px 8px" }}>
|
||||
{!collapsed && (
|
||||
<div style={{ fontSize: "0.58rem", fontWeight: 600, color: "#a09a90", letterSpacing: "0.1em", textTransform: "uppercase", padding: "6px 10px 8px" }}>
|
||||
Projects
|
||||
</div>
|
||||
)}
|
||||
{projects.map(p => {
|
||||
const isActive = activeProjectId === p.id;
|
||||
const color = p.status === "live" ? "#2e7d32" : p.status === "building" ? "#3d5afe" : "#d4a04a";
|
||||
return (
|
||||
<Link key={p.id} href={`/${workspace}/project/${p.id}/overview`}
|
||||
title={collapsed ? p.productName : undefined}
|
||||
style={{
|
||||
width: "100%", display: "flex", alignItems: "center",
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
gap: 9, padding: collapsed ? "9px 0" : "7px 10px",
|
||||
borderRadius: 6,
|
||||
background: isActive ? "#f6f4f0" : "transparent",
|
||||
color: "#1a1a1a", fontSize: "0.8rem",
|
||||
fontWeight: isActive ? 600 : 450,
|
||||
transition: "background 0.12s", textDecoration: "none", overflow: "hidden",
|
||||
}}
|
||||
onMouseEnter={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "#f6f4f0"; }}
|
||||
onMouseLeave={e => { if (!isActive) (e.currentTarget as HTMLElement).style.background = "transparent"; }}
|
||||
>
|
||||
<span style={{ width: 7, height: 7, borderRadius: "50%", background: color, display: "inline-block", flexShrink: 0 }} />
|
||||
{!collapsed && (
|
||||
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{p.productName}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{projects.map((p) => {
|
||||
const isActive = activeProjectId === p.id;
|
||||
return (
|
||||
<Link
|
||||
key={p.id}
|
||||
href={`/${workspace}/project/${p.id}/overview`}
|
||||
title={collapsed ? p.productName : undefined}
|
||||
style={{
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
gap: 9,
|
||||
padding: collapsed ? "9px 0" : "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} />
|
||||
{!collapsed && (
|
||||
<span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
|
||||
{p.productName}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* User footer */}
|
||||
{/* ── User footer ── */}
|
||||
<div style={{
|
||||
padding: collapsed ? "12px 0" : "14px 18px",
|
||||
padding: collapsed ? "10px 0" : "12px 14px",
|
||||
borderTop: "1px solid #eae6de",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
display: "flex", alignItems: "center",
|
||||
justifyContent: collapsed ? "center" : "flex-start",
|
||||
gap: 9,
|
||||
flexShrink: 0,
|
||||
gap: 9, flexShrink: 0,
|
||||
}}>
|
||||
<div
|
||||
title={collapsed ? (session?.user?.name ?? session?.user?.email ?? "Account") : undefined}
|
||||
<div title={collapsed ? (session?.user?.name ?? session?.user?.email ?? "Account") : undefined}
|
||||
style={{
|
||||
width: 28, height: 28, borderRadius: "50%",
|
||||
width: 26, height: 26, borderRadius: "50%",
|
||||
background: "#f0ece4", display: "flex", alignItems: "center",
|
||||
justifyContent: "center", fontSize: "0.72rem", fontWeight: 600,
|
||||
justifyContent: "center", fontSize: "0.7rem", fontWeight: 600,
|
||||
color: "#8a8478", flexShrink: 0, cursor: "default",
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{userInitial}
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
fontSize: "0.78rem", fontWeight: 500, color: "#1a1a1a",
|
||||
overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
|
||||
}}>
|
||||
<div style={{ fontSize: "0.76rem", 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",
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user