301 lines
13 KiB
JavaScript
301 lines
13 KiB
JavaScript
// ============================================================
|
|
// page-admin.jsx — Workspace settings, Members tab.
|
|
// Sub-nav (Workspace / Members / Roles / Integrations / Billing
|
|
// / API) + searchable member table + bulk actions + invite row.
|
|
// ============================================================
|
|
|
|
const AdminBody = ({ theme = "light", hideSubnav = false }) => {
|
|
const dark = theme === "dark";
|
|
const c = dark ? {
|
|
bg: "#0f0f14", panel: "#13131a", border: "#ffffff10",
|
|
text: "#e8e8ee", subtext: "#9a9aa6", muted: "#6a6a78",
|
|
rowAlt: "#ffffff04", input: "#08080c", accent: "#7a78ff",
|
|
chipBg: "#ffffff08", chipText: "#dcdce4", danger: "#ff4d5e",
|
|
} : {
|
|
bg: "#fafaf9", panel: "#ffffff", border: "#ebebe6",
|
|
text: "#111", subtext: "#5a5a5e", muted: "#8a8a90",
|
|
rowAlt: "#fafaf6", input: "#fff", accent: "#5e5cff",
|
|
chipBg: "#f1f0eb", chipText: "#3a3a3e", danger: "#dc2626",
|
|
};
|
|
|
|
const subnav = [
|
|
"General", "Members", "Roles", "Integrations", "Billing", "API & Webhooks", "Audit log",
|
|
];
|
|
|
|
const roleColors = {
|
|
Owner: "#b15bff",
|
|
Admin: "#5e5cff",
|
|
Member: "#22c55e",
|
|
Guest: "#9a9aa6",
|
|
};
|
|
|
|
const members = [
|
|
{ i: "MR", c: "#d4b8a8", n: "Mira Reyes", e: "mira@lattice.co", r: "Owner", s: "Active", last: "now", teams: ["Founding"] },
|
|
{ i: "TR", c: "#c8e8a8", n: "Theo Roux", e: "theo@lattice.co", r: "Admin", s: "Active", last: "12 min", teams: ["Engineering"] },
|
|
{ i: "DP", c: "#a8c8e8", n: "Devi Patel", e: "devi@lattice.co", r: "Admin", s: "Active", last: "1 hour", teams: ["Revenue"] },
|
|
{ i: "SK", c: "#e8a87c", n: "Sun Kim", e: "sun@lattice.co", r: "Member", s: "Active", last: "today", teams: ["Revenue", "Design"] },
|
|
{ i: "AN", c: "#e8c8a8", n: "Ade Nwosu", e: "ade@lattice.co", r: "Member", s: "Active", last: "yesterday", teams: ["Engineering"] },
|
|
{ i: "LB", c: "#c8a8e8", n: "Linnea Berg", e: "linnea@lattice.co", r: "Member", s: "Invited", last: "—", teams: [] },
|
|
{ i: "JF", c: "#a8e8c8", n: "Jamal Frost", e: "jamal@partner.co", r: "Guest", s: "Active", last: "3 days", teams: ["Revenue"] },
|
|
{ i: "ER", c: "#e8a8c8", n: "Elin Roos", e: "elin@lattice.co", r: "Member", s: "Suspended", last: "14 days", teams: ["Design"] },
|
|
];
|
|
|
|
const Badge = ({ color, children, dot }) => (
|
|
<span style={{
|
|
display: "inline-flex", alignItems: "center", gap: 5,
|
|
padding: "2px 8px", borderRadius: 999,
|
|
background: color ? `${color}1f` : c.chipBg,
|
|
color: color || c.chipText,
|
|
fontSize: 11, fontWeight: 500, whiteSpace: "nowrap",
|
|
}}>
|
|
{dot && <span style={{
|
|
width: 6, height: 6, borderRadius: "50%", background: color,
|
|
}}></span>}
|
|
{children}
|
|
</span>
|
|
);
|
|
|
|
const Avatar = ({ name, color, size = 28 }) => (
|
|
<div style={{
|
|
width: size, height: size, borderRadius: "50%", background: color,
|
|
fontSize: size * 0.4, fontWeight: 600, color: "#3a2820",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
flexShrink: 0,
|
|
}}>{name}</div>
|
|
);
|
|
|
|
return (
|
|
<div style={{
|
|
height: "100%", background: c.bg, color: c.text, fontFamily: SANS,
|
|
display: "grid",
|
|
gridTemplateColumns: hideSubnav ? "1fr" : "220px 1fr",
|
|
overflow: "hidden",
|
|
}}>
|
|
{/* Settings sub-nav */}
|
|
{!hideSubnav && <aside style={{
|
|
borderRight: `1px solid ${c.border}`, padding: "20px 12px",
|
|
background: dark ? "#0a0a10" : "#f5f5f2",
|
|
display: "flex", flexDirection: "column",
|
|
}}>
|
|
<div style={{
|
|
fontSize: 11, color: c.muted, padding: "0 10px 8px",
|
|
letterSpacing: "0.06em", textTransform: "uppercase", fontWeight: 500,
|
|
}}>Settings</div>
|
|
{subnav.map((s, i) => (
|
|
<div key={s} style={{
|
|
padding: "7px 10px", borderRadius: 6, fontSize: 13, cursor: "pointer",
|
|
color: i === 1 ? c.text : c.subtext,
|
|
background: i === 1 ? (dark ? "#ffffff10" : "#fff") : "transparent",
|
|
boxShadow: i === 1 && !dark ? "0 1px 0 #00000008, 0 0 0 1px #00000008" : "none",
|
|
fontWeight: i === 1 ? 500 : 400,
|
|
marginBottom: 2,
|
|
}}>{s}</div>
|
|
))}
|
|
<div style={{
|
|
fontSize: 11, color: c.muted, padding: "16px 10px 8px",
|
|
letterSpacing: "0.06em", textTransform: "uppercase", fontWeight: 500,
|
|
}}>Personal</div>
|
|
{["Profile", "Notifications", "Sessions"].map(s => (
|
|
<div key={s} style={{
|
|
padding: "7px 10px", borderRadius: 6, fontSize: 13, cursor: "pointer",
|
|
color: c.subtext, marginBottom: 2,
|
|
}}>{s}</div>
|
|
))}
|
|
|
|
<div style={{ flex: 1 }}></div>
|
|
<div style={{
|
|
padding: "12px 12px", borderRadius: 8,
|
|
background: dark ? "#ffffff06" : "#fff",
|
|
border: `1px solid ${c.border}`,
|
|
}}>
|
|
<div style={{ fontSize: 12, fontWeight: 500, marginBottom: 4 }}>
|
|
Free workspace
|
|
</div>
|
|
<div style={{ fontSize: 11, color: c.muted, lineHeight: 1.4, marginBottom: 10 }}>
|
|
6 of 10 seats used. Upgrade for SSO, audit log retention, and SCIM.
|
|
</div>
|
|
<button style={{
|
|
width: "100%", padding: "7px 12px", borderRadius: 6,
|
|
background: dark ? "#fff" : "#111", color: dark ? "#111" : "#fff",
|
|
border: "none", fontSize: 12, fontFamily: SANS, fontWeight: 500,
|
|
cursor: "pointer",
|
|
}}>Upgrade to Pro →</button>
|
|
</div>
|
|
</aside>}
|
|
|
|
{/* Main */}
|
|
<div style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}>
|
|
{/* Page header */}
|
|
<div style={{
|
|
padding: "20px 28px 14px", borderBottom: `1px solid ${c.border}`,
|
|
}}>
|
|
<div style={{ fontSize: 12, color: c.muted, marginBottom: 6 }}>
|
|
Settings / Members
|
|
</div>
|
|
<div style={{
|
|
display: "flex", justifyContent: "space-between", alignItems: "flex-end",
|
|
}}>
|
|
<div>
|
|
<h1 style={{ fontSize: 24, fontWeight: 600, margin: 0, letterSpacing: "-0.02em" }}>
|
|
Members
|
|
</h1>
|
|
<p style={{
|
|
fontSize: 13, color: c.subtext, margin: "6px 0 0", maxWidth: 540,
|
|
}}>
|
|
Manage who has access to <b>Lattice Studio</b>. Roles control
|
|
what each person can see and edit across the workspace.
|
|
</p>
|
|
</div>
|
|
<div style={{ display: "flex", gap: 8 }}>
|
|
<button style={{
|
|
padding: "8px 14px", borderRadius: 6, fontSize: 13, fontFamily: SANS,
|
|
background: c.panel, border: `1px solid ${c.border}`, color: c.text,
|
|
cursor: "pointer",
|
|
}}>Export CSV</button>
|
|
<button style={{
|
|
padding: "8px 14px", borderRadius: 6, fontSize: 13, fontFamily: SANS,
|
|
background: dark ? "#fff" : "#111", color: dark ? "#111" : "#fff",
|
|
border: "none", cursor: "pointer", fontWeight: 500,
|
|
display: "flex", alignItems: "center", gap: 6,
|
|
}}><Icon d={P.plus} size={13}/> Invite people</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter / search row */}
|
|
<div style={{
|
|
padding: "12px 28px", borderBottom: `1px solid ${c.border}`,
|
|
display: "flex", alignItems: "center", gap: 10,
|
|
}}>
|
|
<div style={{
|
|
display: "flex", alignItems: "center", gap: 8, padding: "6px 10px",
|
|
background: c.input, border: `1px solid ${c.border}`, borderRadius: 6,
|
|
fontSize: 12, color: c.muted, width: 280,
|
|
}}>
|
|
<Icon d={P.search} size={13} />
|
|
<span style={{ flex: 1 }}>Search by name, email…</span>
|
|
</div>
|
|
{[
|
|
{ l: "Role", v: "All" },
|
|
{ l: "Status", v: "Active + Invited" },
|
|
{ l: "Team", v: "Any" },
|
|
].map(f => (
|
|
<div key={f.l} style={{
|
|
display: "flex", alignItems: "center", gap: 6, padding: "6px 10px",
|
|
border: `1px dashed ${c.border}`, borderRadius: 6, fontSize: 12,
|
|
color: c.subtext, cursor: "pointer",
|
|
}}>
|
|
<span style={{ color: c.muted }}>{f.l}:</span>
|
|
<span style={{ color: c.text, fontWeight: 500 }}>{f.v}</span>
|
|
<Icon d={P.chevron} size={11} />
|
|
</div>
|
|
))}
|
|
<div style={{ flex: 1 }}></div>
|
|
<span style={{ fontSize: 12, color: c.muted }}>
|
|
<b style={{ color: c.text }}>8</b> members · 1 invited · 1 suspended
|
|
</span>
|
|
</div>
|
|
|
|
{/* Table */}
|
|
<div style={{ flex: 1, overflowY: "auto" }}>
|
|
<div style={{
|
|
display: "grid",
|
|
gridTemplateColumns: "28px 2fr 1fr 1fr 1.4fr 1fr 32px",
|
|
padding: "10px 28px", fontSize: 11, color: c.muted,
|
|
letterSpacing: "0.04em", textTransform: "uppercase", fontWeight: 500,
|
|
borderBottom: `1px solid ${c.border}`,
|
|
alignItems: "center", gap: 12,
|
|
}}>
|
|
<input type="checkbox" style={{ accentColor: c.accent }} readOnly />
|
|
<span>Name</span>
|
|
<span>Role</span>
|
|
<span>Status</span>
|
|
<span>Teams</span>
|
|
<span>Last active</span>
|
|
<span></span>
|
|
</div>
|
|
{members.map((m, i) => (
|
|
<div key={m.e} style={{
|
|
display: "grid",
|
|
gridTemplateColumns: "28px 2fr 1fr 1fr 1.4fr 1fr 32px",
|
|
padding: "10px 28px", fontSize: 13,
|
|
alignItems: "center", gap: 12,
|
|
borderBottom: `1px solid ${c.border}`,
|
|
background: i % 2 === 1 ? c.rowAlt : "transparent",
|
|
}}>
|
|
<input type="checkbox" style={{ accentColor: c.accent }} readOnly />
|
|
<div style={{ display: "flex", alignItems: "center", gap: 10, minWidth: 0 }}>
|
|
<Avatar name={m.i} color={m.c} />
|
|
<div style={{ minWidth: 0 }}>
|
|
<div style={{ fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{m.n}</div>
|
|
<div style={{ fontSize: 11, color: c.muted, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{m.e}</div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div style={{
|
|
display: "inline-flex", alignItems: "center", gap: 6,
|
|
padding: "3px 9px", borderRadius: 5,
|
|
background: `${roleColors[m.r]}18`,
|
|
color: roleColors[m.r], fontSize: 12, fontWeight: 500,
|
|
cursor: "pointer",
|
|
}}>
|
|
{m.r}
|
|
<Icon d={P.chevron} size={11} />
|
|
</div>
|
|
</div>
|
|
<div>
|
|
{m.s === "Active" && <Badge color="#22c55e" dot>Active</Badge>}
|
|
{m.s === "Invited" && <Badge color="#f6c560" dot>Invited</Badge>}
|
|
{m.s === "Suspended" && <Badge color={c.danger} dot>Suspended</Badge>}
|
|
</div>
|
|
<div style={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
|
|
{m.teams.length === 0
|
|
? <span style={{ fontSize: 12, color: c.muted }}>—</span>
|
|
: m.teams.map(t => <Badge key={t}>{t}</Badge>)}
|
|
</div>
|
|
<div style={{ fontSize: 12, color: c.subtext }}>{m.last}</div>
|
|
<div style={{ color: c.muted, display: "flex", justifyContent: "flex-end", cursor: "pointer" }}>
|
|
<Icon d={P.more} size={16} />
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* Pending-invite footer band */}
|
|
<div style={{
|
|
margin: "18px 28px 28px", padding: "14px 16px", borderRadius: 10,
|
|
background: dark ? "#ffffff06" : "#fff8e6",
|
|
border: `1px solid ${dark ? "#ffffff14" : "#f3e0a4"}`,
|
|
display: "flex", alignItems: "center", gap: 14,
|
|
}}>
|
|
<div style={{
|
|
width: 32, height: 32, borderRadius: 8,
|
|
background: dark ? "#ffffff10" : "#f6c56020",
|
|
color: dark ? "#f6c560" : "#a87b1a",
|
|
display: "flex", alignItems: "center", justifyContent: "center",
|
|
}}><Icon d={P.bell} size={16} /></div>
|
|
<div style={{ flex: 1 }}>
|
|
<div style={{ fontSize: 13, fontWeight: 500 }}>
|
|
1 invitation is still pending
|
|
</div>
|
|
<div style={{ fontSize: 12, color: c.subtext, marginTop: 2 }}>
|
|
<b>linnea@lattice.co</b> hasn't accepted yet — sent 3 days ago.
|
|
</div>
|
|
</div>
|
|
<button style={{
|
|
padding: "6px 12px", borderRadius: 6, fontSize: 12, fontFamily: SANS,
|
|
background: dark ? "#ffffff10" : "#fff",
|
|
border: `1px solid ${c.border}`, color: c.text, cursor: "pointer",
|
|
}}>Resend</button>
|
|
<button style={{
|
|
padding: "6px 12px", borderRadius: 6, fontSize: 12, fontFamily: SANS,
|
|
background: "transparent", border: "none", color: c.muted, cursor: "pointer",
|
|
}}>Revoke</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
window.AdminBody = AdminBody;
|