feat(agency): refactor agency dashboard into a Next.js App Router layout with dynamic route-based views for individual clients
This commit is contained in:
186
vibn-frontend/app/[workspace]/agency/client/[clientId]/page.tsx
Normal file
186
vibn-frontend/app/[workspace]/agency/client/[clientId]/page.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
|
||||
// The client list is currently mocked, but in the future this would be fetched from the DB
|
||||
const CLIENTS = {
|
||||
apex: { name: "Apex Plumbing" },
|
||||
barton: { name: "Barton Creek HVAC" },
|
||||
};
|
||||
|
||||
export default function ClientViewPage() {
|
||||
const params = useParams();
|
||||
const clientId = params.clientId as keyof typeof CLIENTS;
|
||||
const client = CLIENTS[clientId] || { name: "Unknown Client" };
|
||||
|
||||
return (
|
||||
<div style={{ padding: "40px 56px", maxWidth: 1100, margin: "0 auto" }}>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
marginBottom: 32,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
fontWeight: 600,
|
||||
color: "#a09a90",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.04em",
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
Client
|
||||
</div>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: "28px",
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
letterSpacing: "-0.02em",
|
||||
margin: "0 0 8px",
|
||||
}}
|
||||
>
|
||||
{client.name}
|
||||
</h1>
|
||||
<p style={{ color: "#6b6560", fontSize: "15px", margin: 0 }}>
|
||||
Manage projects, infrastructure, and secrets for this client.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
background: "var(--accent)",
|
||||
color: "#fff",
|
||||
borderRadius: 8,
|
||||
fontWeight: 500,
|
||||
fontSize: "13px",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
boxShadow: "0 2px 8px var(--accent-glow)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, lineHeight: 0 }}>+</span> New project
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 24,
|
||||
borderBottom: "1px solid #eae6de",
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
{["Projects", "Infrastructure", "Secrets Vault", "Contacts"].map(
|
||||
(tab, i) => (
|
||||
<div
|
||||
key={tab}
|
||||
style={{
|
||||
paddingBottom: 12,
|
||||
fontSize: "14px",
|
||||
fontWeight: i === 0 ? 600 : 500,
|
||||
color: i === 0 ? "#1a1a1a" : "#6b6560",
|
||||
borderBottom:
|
||||
i === 0 ? "2px solid var(--accent)" : "2px solid transparent",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{tab}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Projects Grid */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr", gap: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #eae6de",
|
||||
borderRadius: 12,
|
||||
padding: "20px 24px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.02)",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
background: "#f6f4f0",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
color: "#1a1a1a",
|
||||
fontWeight: 600,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{client.name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
style={{
|
||||
fontSize: "16px",
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
margin: "0 0 4px",
|
||||
}}
|
||||
>
|
||||
Customer Portal
|
||||
</h3>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
color: "#6b6560",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: "50%",
|
||||
background: "#2e7d32",
|
||||
}}
|
||||
/>
|
||||
Live on {client.name.toLowerCase().replace(/[^a-z0-9]/g, "")}.vibn.app
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
style={{
|
||||
padding: "6px 12px",
|
||||
border: "1px solid #eae6de",
|
||||
background: "transparent",
|
||||
borderRadius: 6,
|
||||
fontSize: "13px",
|
||||
fontWeight: 500,
|
||||
color: "#1a1a1a",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Open Builder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
264
vibn-frontend/app/[workspace]/agency/layout.tsx
Normal file
264
vibn-frontend/app/[workspace]/agency/layout.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import Link from "next/link";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
|
||||
// Minimal SVG Icons
|
||||
const Icons = {
|
||||
Search: () => <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>,
|
||||
Target: () => <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>,
|
||||
MessageCircle: () => <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>,
|
||||
Kanban: () => <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><path d="M8 7v7"/><path d="M12 7v4"/><path d="M16 7v9"/></svg>,
|
||||
FileText: () => <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>,
|
||||
Users: () => <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>,
|
||||
Folder: () => <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/></svg>,
|
||||
CreditCard: () => <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="1" y="4" width="22" height="16" rx="2" ry="2"/><line x1="1" y1="10" x2="23" y2="10"/></svg>,
|
||||
TrendingDown: () => <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><polyline points="22 17 13.5 8.5 8.5 13.5 2 7"/><polyline points="16 17 22 17 22 11"/></svg>,
|
||||
Settings: () => <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>,
|
||||
};
|
||||
|
||||
function NavItem({
|
||||
icon: Icon,
|
||||
label,
|
||||
active,
|
||||
badge,
|
||||
href,
|
||||
}: {
|
||||
icon?: React.ElementType;
|
||||
label: string;
|
||||
active?: boolean;
|
||||
badge?: string;
|
||||
href: string;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
padding: "7px 10px",
|
||||
borderRadius: 6,
|
||||
background: active ? "#f6f4f0" : "transparent",
|
||||
color: active ? "#1a1a1a" : "#6b6560",
|
||||
fontWeight: active ? 600 : 500,
|
||||
fontSize: "13px",
|
||||
transition: "background 0.15s",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
textDecoration: "none",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!active) e.currentTarget.style.background = "#f6f4f0";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!active) e.currentTarget.style.background = "transparent";
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
{Icon && (
|
||||
<span
|
||||
style={{
|
||||
opacity: active ? 0.9 : 0.5,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
}}
|
||||
>
|
||||
<Icon />
|
||||
</span>
|
||||
)}
|
||||
{label}
|
||||
</div>
|
||||
{badge && (
|
||||
<span
|
||||
style={{
|
||||
background: "var(--accent)",
|
||||
color: "#fff",
|
||||
fontSize: "10px",
|
||||
fontWeight: 700,
|
||||
padding: "2px 6px",
|
||||
borderRadius: 99,
|
||||
}}
|
||||
>
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionHeader({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
fontWeight: 600,
|
||||
color: "#a09a90",
|
||||
letterSpacing: "0.06em",
|
||||
textTransform: "uppercase",
|
||||
padding: "16px 10px 6px",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AgencyLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const params = useParams();
|
||||
const pathname = usePathname();
|
||||
const workspace = params.workspace as string;
|
||||
|
||||
// The client list is currently mocked, but in the future this would be fetched from the DB
|
||||
const clients = [
|
||||
{ id: "apex", name: "Apex Plumbing" },
|
||||
{ id: "barton", name: "Barton Creek HVAC" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
background: "#fcfbfa",
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
{/* ── LEFT SIDEBAR (Cadence Style) ── */}
|
||||
<div
|
||||
style={{
|
||||
width: 240,
|
||||
borderRight: "1px solid #eae6de",
|
||||
background: "#f5f4ef",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* Workspace Switcher */}
|
||||
<div
|
||||
style={{
|
||||
padding: "16px 16px 12px",
|
||||
borderBottom: "1px solid #eae6de",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 6,
|
||||
background: "var(--accent)",
|
||||
color: "#fff",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
fontWeight: 700,
|
||||
fontSize: "12px",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
{typeof workspace === "string" ? workspace.charAt(0) : "A"}
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
lineHeight: 1.2,
|
||||
textTransform: "capitalize",
|
||||
}}
|
||||
>
|
||||
{typeof workspace === "string"
|
||||
? workspace.replace(/-/g, " ")
|
||||
: "Atlas Agency"}
|
||||
</span>
|
||||
<span style={{ fontSize: "12px", color: "#6b6560" }}>
|
||||
Pro · 2 members
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global Search */}
|
||||
<div style={{ padding: "12px 12px 0" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
background: "#fff",
|
||||
border: "1px solid #eae6de",
|
||||
borderRadius: 6,
|
||||
padding: "6px 10px",
|
||||
color: "#a09a90",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
<Icons.Search />
|
||||
<span>Search or jump to...</span>
|
||||
<kbd
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
fontSize: "10px",
|
||||
fontFamily: "var(--font-mono)",
|
||||
background: "#f5f4ef",
|
||||
padding: "2px 4px",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
⌘K
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: "8px 8px 24px" }}>
|
||||
<SectionHeader>Growth</SectionHeader>
|
||||
<NavItem
|
||||
icon={Icons.Target}
|
||||
label="Find Clients"
|
||||
href={`/${workspace}/agency`}
|
||||
active={pathname === `/${workspace}/agency`}
|
||||
/>
|
||||
<NavItem icon={Icons.MessageCircle} label="Outreach" href="#" />
|
||||
<NavItem icon={Icons.Kanban} label="Pipeline" badge="3" href="#" />
|
||||
<NavItem icon={Icons.FileText} label="Proposals & Contracts" href="#" />
|
||||
<NavItem icon={Icons.Users} label="Contacts" href="#" />
|
||||
|
||||
<SectionHeader>Clients</SectionHeader>
|
||||
|
||||
{clients.map((client) => {
|
||||
const clientPath = `/${workspace}/agency/client/${client.id}`;
|
||||
const isActive = pathname.startsWith(clientPath);
|
||||
return (
|
||||
<NavItem
|
||||
key={client.id}
|
||||
icon={Icons.Folder}
|
||||
label={client.name}
|
||||
href={clientPath}
|
||||
active={isActive}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
<SectionHeader>Finance & Ops</SectionHeader>
|
||||
<NavItem icon={Icons.CreditCard} label="Invoices & Payments" href="#" />
|
||||
<NavItem icon={Icons.TrendingDown} label="Expenses" href="#" />
|
||||
<NavItem icon={Icons.Users} label="Users" href="#" />
|
||||
<NavItem icon={Icons.Settings} label="Settings" href="#" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── MAIN CONTENT AREA ── */}
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,90 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import React from "react";
|
||||
|
||||
// Minimal SVG Icons
|
||||
const Icons = {
|
||||
Search: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<path d="m21 21-4.3-4.3" />
|
||||
</svg>
|
||||
),
|
||||
Target: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<circle cx="12" cy="12" r="6" />
|
||||
<circle cx="12" cy="12" r="2" />
|
||||
</svg>
|
||||
),
|
||||
MessageCircle: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
|
||||
</svg>
|
||||
),
|
||||
Kanban: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<path d="M8 7v7" />
|
||||
<path d="M12 7v4" />
|
||||
<path d="M16 7v9" />
|
||||
</svg>
|
||||
),
|
||||
FileText: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
||||
<path d="M14 2v6h6" />
|
||||
<line x1="16" y1="13" x2="8" y2="13" />
|
||||
<line x1="16" y1="17" x2="8" y2="17" />
|
||||
<polyline points="10 9 9 9 8 9" />
|
||||
</svg>
|
||||
),
|
||||
Users: () => (
|
||||
<svg
|
||||
width="14"
|
||||
@@ -102,575 +21,9 @@ const Icons = {
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||
</svg>
|
||||
),
|
||||
Folder: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.93a2 2 0 0 1-1.66-.9l-.82-1.2A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z" />
|
||||
</svg>
|
||||
),
|
||||
Box: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
|
||||
<line x1="12" y1="22.08" x2="12" y2="12" />
|
||||
</svg>
|
||||
),
|
||||
Server: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2" />
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2" />
|
||||
<line x1="6" y1="6" x2="6.01" y2="6" />
|
||||
<line x1="6" y1="18" x2="6.01" y2="18" />
|
||||
</svg>
|
||||
),
|
||||
Repeat: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m17 2 4 4-4 4" />
|
||||
<path d="M3 11v-1a4 4 0 0 1 4-4h14" />
|
||||
<path d="m7 22-4-4 4-4" />
|
||||
<path d="M21 13v1a4 4 0 0 1-4 4H3" />
|
||||
</svg>
|
||||
),
|
||||
CreditCard: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
|
||||
<line x1="1" y1="10" x2="23" y2="10" />
|
||||
</svg>
|
||||
),
|
||||
TrendingDown: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="22 17 13.5 8.5 8.5 13.5 2 7" />
|
||||
<polyline points="16 17 22 17 22 11" />
|
||||
</svg>
|
||||
),
|
||||
Key: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4" />
|
||||
</svg>
|
||||
),
|
||||
Settings: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||
</svg>
|
||||
),
|
||||
ChevronDown: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
),
|
||||
ChevronRight: () => (
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
function NavItem({
|
||||
icon: Icon,
|
||||
label,
|
||||
active,
|
||||
badge,
|
||||
inset = false,
|
||||
}: {
|
||||
icon?: React.ElementType;
|
||||
label: string;
|
||||
active?: boolean;
|
||||
badge?: string;
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
className="nav-item"
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
padding: "7px 10px",
|
||||
paddingLeft: inset ? 32 : 10,
|
||||
borderRadius: 6,
|
||||
background: active ? "#f6f4f0" : "transparent",
|
||||
color: active ? "#1a1a1a" : "#6b6560",
|
||||
fontWeight: active ? 600 : 500,
|
||||
fontSize: "13px",
|
||||
transition: "background 0.15s",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (!active) e.currentTarget.style.background = "#f6f4f0";
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (!active) e.currentTarget.style.background = "transparent";
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
{Icon && (
|
||||
<span
|
||||
style={{
|
||||
opacity: active ? 0.9 : 0.5,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
}}
|
||||
>
|
||||
<Icon />
|
||||
</span>
|
||||
)}
|
||||
{label}
|
||||
</div>
|
||||
{badge && (
|
||||
<span
|
||||
style={{
|
||||
background: "var(--accent)",
|
||||
color: "#fff",
|
||||
fontSize: "10px",
|
||||
fontWeight: 700,
|
||||
padding: "2px 6px",
|
||||
borderRadius: 99,
|
||||
}}
|
||||
>
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionHeader({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "10px",
|
||||
fontWeight: 600,
|
||||
color: "#a09a90",
|
||||
letterSpacing: "0.06em",
|
||||
textTransform: "uppercase",
|
||||
padding: "16px 10px 6px",
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AgencyDashboard() {
|
||||
const { workspace } = useParams();
|
||||
const [activeTab, setActiveTab] = useState("find_clients");
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
height: "100vh",
|
||||
background: "#fcfbfa",
|
||||
fontFamily: "var(--font-sans)",
|
||||
}}
|
||||
>
|
||||
{/* ── LEFT SIDEBAR (Cadence Style) ── */}
|
||||
<div
|
||||
style={{
|
||||
width: 240,
|
||||
borderRight: "1px solid #eae6de",
|
||||
background: "#f5f4ef",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* Workspace Switcher */}
|
||||
<div
|
||||
style={{
|
||||
padding: "16px 16px 12px",
|
||||
borderBottom: "1px solid #eae6de",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 6,
|
||||
background: "var(--accent)",
|
||||
color: "#fff",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
fontWeight: 700,
|
||||
fontSize: "12px",
|
||||
textTransform: "uppercase",
|
||||
}}
|
||||
>
|
||||
{typeof workspace === "string" ? workspace.charAt(0) : "A"}
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<span
|
||||
style={{
|
||||
fontSize: "14px",
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
lineHeight: 1.2,
|
||||
textTransform: "capitalize",
|
||||
}}
|
||||
>
|
||||
{typeof workspace === "string"
|
||||
? workspace.replace(/-/g, " ")
|
||||
: "Atlas Agency"}
|
||||
</span>
|
||||
<span style={{ fontSize: "12px", color: "#6b6560" }}>
|
||||
Pro · 2 members
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Global Search */}
|
||||
<div style={{ padding: "12px 12px 0" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
background: "#fff",
|
||||
border: "1px solid #eae6de",
|
||||
borderRadius: 6,
|
||||
padding: "6px 10px",
|
||||
color: "#a09a90",
|
||||
fontSize: "13px",
|
||||
}}
|
||||
>
|
||||
<Icons.Search />
|
||||
<span>Search or jump to...</span>
|
||||
<kbd
|
||||
style={{
|
||||
marginLeft: "auto",
|
||||
fontSize: "10px",
|
||||
fontFamily: "var(--font-mono)",
|
||||
background: "#f5f4ef",
|
||||
padding: "2px 4px",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
⌘K
|
||||
</kbd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<div style={{ flex: 1, overflowY: "auto", padding: "8px 8px 24px" }}>
|
||||
<SectionHeader>Growth</SectionHeader>
|
||||
<div onClick={() => setActiveTab("find_clients")}>
|
||||
<NavItem
|
||||
icon={Icons.Target}
|
||||
label="Find Clients"
|
||||
active={activeTab === "find_clients"}
|
||||
/>
|
||||
</div>
|
||||
<NavItem icon={Icons.MessageCircle} label="Outreach" />
|
||||
<NavItem icon={Icons.Kanban} label="Pipeline" badge="3" />
|
||||
<NavItem icon={Icons.FileText} label="Proposals & Contracts" />
|
||||
<NavItem icon={Icons.Users} label="Contacts" />
|
||||
|
||||
<SectionHeader>Clients</SectionHeader>
|
||||
|
||||
<div onClick={() => setActiveTab("client_apex")}>
|
||||
<NavItem
|
||||
icon={Icons.Folder}
|
||||
label="Apex Plumbing"
|
||||
active={activeTab === "client_apex"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div onClick={() => setActiveTab("client_barton")}>
|
||||
<NavItem
|
||||
icon={Icons.Folder}
|
||||
label="Barton Creek HVAC"
|
||||
active={activeTab === "client_barton"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SectionHeader>Finance & Ops</SectionHeader>
|
||||
<NavItem icon={Icons.CreditCard} label="Invoices & Payments" />
|
||||
<NavItem icon={Icons.TrendingDown} label="Expenses" />
|
||||
<NavItem icon={Icons.Users} label="Users" />
|
||||
<NavItem icon={Icons.Settings} label="Settings" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── MAIN CONTENT AREA ── */}
|
||||
<div style={{ flex: 1, overflowY: "auto" }}>
|
||||
{activeTab === "find_clients" && <FindClientsView />}
|
||||
{activeTab === "client_apex" && <ClientView name="Apex Plumbing" />}
|
||||
{activeTab === "client_barton" && (
|
||||
<ClientView name="Barton Creek HVAC" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── CLIENT VIEW (Projects, Infra, Secrets) ──
|
||||
function ClientView({ name }: { name: string }) {
|
||||
return (
|
||||
<div style={{ padding: "40px 56px", maxWidth: 1100, margin: "0 auto" }}>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "flex-start",
|
||||
marginBottom: 32,
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "11px",
|
||||
fontWeight: 600,
|
||||
color: "#a09a90",
|
||||
textTransform: "uppercase",
|
||||
letterSpacing: "0.04em",
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
Client
|
||||
</div>
|
||||
<h1
|
||||
style={{
|
||||
fontSize: "28px",
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
letterSpacing: "-0.02em",
|
||||
margin: "0 0 8px",
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</h1>
|
||||
<p style={{ color: "#6b6560", fontSize: "15px", margin: 0 }}>
|
||||
Manage projects, infrastructure, and secrets for this client.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
background: "var(--accent)",
|
||||
color: "#fff",
|
||||
borderRadius: 8,
|
||||
fontWeight: 500,
|
||||
fontSize: "13px",
|
||||
border: "none",
|
||||
cursor: "pointer",
|
||||
boxShadow: "0 2px 8px var(--accent-glow)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 16, lineHeight: 0 }}>+</span> New project
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
gap: 24,
|
||||
borderBottom: "1px solid #eae6de",
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
{["Projects", "Infrastructure", "Secrets Vault", "Contacts"].map(
|
||||
(tab, i) => (
|
||||
<div
|
||||
key={tab}
|
||||
style={{
|
||||
paddingBottom: 12,
|
||||
fontSize: "14px",
|
||||
fontWeight: i === 0 ? 600 : 500,
|
||||
color: i === 0 ? "#1a1a1a" : "#6b6560",
|
||||
borderBottom:
|
||||
i === 0 ? "2px solid var(--accent)" : "2px solid transparent",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{tab}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Projects Grid */}
|
||||
<div style={{ display: "grid", gridTemplateColumns: "1fr", gap: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
background: "#fff",
|
||||
border: "1px solid #eae6de",
|
||||
borderRadius: 12,
|
||||
padding: "20px 24px",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
boxShadow: "0 2px 8px rgba(0,0,0,0.02)",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 16 }}>
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
background: "#f6f4f0",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
color: "#1a1a1a",
|
||||
fontWeight: 600,
|
||||
fontSize: 16,
|
||||
}}
|
||||
>
|
||||
{name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<h3
|
||||
style={{
|
||||
fontSize: "16px",
|
||||
fontWeight: 600,
|
||||
color: "#1a1a1a",
|
||||
margin: "0 0 4px",
|
||||
}}
|
||||
>
|
||||
Customer Portal
|
||||
</h3>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "13px",
|
||||
color: "#6b6560",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: "inline-block",
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: "50%",
|
||||
background: "#2e7d32",
|
||||
}}
|
||||
/>
|
||||
Live on {name.toLowerCase().replace(/[^a-z0-9]/g, "")}.vibn.app
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
style={{
|
||||
padding: "6px 12px",
|
||||
border: "1px solid #eae6de",
|
||||
background: "transparent",
|
||||
borderRadius: 6,
|
||||
fontSize: "13px",
|
||||
fontWeight: 500,
|
||||
color: "#1a1a1a",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
Open Builder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── FIND CLIENTS VIEW (The Lead Gen Engine) ──
|
||||
function FindClientsView() {
|
||||
export default function FindClientsView() {
|
||||
return (
|
||||
<div style={{ padding: "40px 56px", maxWidth: 1100, margin: "0 auto" }}>
|
||||
{/* Header */}
|
||||
@@ -743,7 +96,7 @@ function FindClientsView() {
|
||||
Ideal Customer Profile
|
||||
</label>
|
||||
<div style={{ fontSize: "14px", fontWeight: 500, color: "#1a1a1a" }}>
|
||||
"Plumbers and HVAC"
|
||||
"Plumbers and HVAC"
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user