init
This commit is contained in:
263
people/desktop/DesktopPeopleTable.js
Normal file
263
people/desktop/DesktopPeopleTable.js
Normal file
@@ -0,0 +1,263 @@
|
||||
class DesktopPeopleTable extends Shadow {
|
||||
constructor(people, selectedId, sortKey, sortDir, onSelect, onSort) {
|
||||
super()
|
||||
this.people = people
|
||||
this.selectedId = selectedId
|
||||
this.sortKey = sortKey
|
||||
this.sortDir = sortDir
|
||||
this.onSelect = onSelect
|
||||
this.onSort = onSort
|
||||
}
|
||||
|
||||
get columns() {
|
||||
return [
|
||||
{ key: "status", label: "", width: "44px", sortable: false },
|
||||
{ key: "name", label: "Name", width: "200px", sortable: true },
|
||||
{ key: "email", label: "Email", width: "220px", sortable: true },
|
||||
{ key: "phone", label: "Phone", width: "140px", sortable: false },
|
||||
{ key: "title", label: "Title", width: "250px", sortable: true },
|
||||
{ key: "county", label: "County", width: "120px", sortable: true },
|
||||
{ key: "roles", label: "Role", width: "120px", sortable: true },
|
||||
{ key: "tier", label: "Tier", width: "110px", sortable: true },
|
||||
{ key: "joined", label: "Joined", width: "120px", sortable: true },
|
||||
{ key: "notes", label: "Notes", width: "1fr", sortable: false },
|
||||
];
|
||||
}
|
||||
|
||||
get gridTemplate() {
|
||||
return this.columns.map(c => c.width).join(" ");
|
||||
}
|
||||
|
||||
render() {
|
||||
VStack(() => {
|
||||
VStack(() => {
|
||||
// ── Sticky header ─────────────────────────────────────────
|
||||
HStack(() => {
|
||||
this.columns.forEach(col => this.renderHeaderCell(col))
|
||||
})
|
||||
.attr({ style: `display: grid; grid-template-columns: ${this.gridTemplate};` })
|
||||
.paddingHorizontal(0.75, em)
|
||||
.paddingVertical(0.52, em)
|
||||
.borderBottom("1px solid var(--divider)")
|
||||
.flexShrink(0)
|
||||
.boxSizing("border-box")
|
||||
|
||||
// ── Rows ──────────────────────────────────────────────────
|
||||
VStack(() => {
|
||||
if (this.people.length === 0) {
|
||||
VStack(() => {
|
||||
p("No members match your filters")
|
||||
.margin(0).fontSize(0.88, em)
|
||||
.color("var(--headertext)").opacity(0.35)
|
||||
})
|
||||
.position("absolute")
|
||||
.top(0).right(0).bottom(0).left(0)
|
||||
.textAlign("center")
|
||||
.justifyContent("center")
|
||||
} else {
|
||||
this.people.forEach((person, i) => this.renderRow(person, i))
|
||||
}
|
||||
})
|
||||
.overflowY("auto")
|
||||
.overflowX("hidden")
|
||||
.flex(1)
|
||||
.minHeight(0)
|
||||
})
|
||||
.width("max-content")
|
||||
.minWidth(100, pct)
|
||||
.height(100, pct)
|
||||
})
|
||||
.height(100, pct).width(100, pct).overflowX("auto").overflowY("hidden")
|
||||
.position("relative")
|
||||
}
|
||||
|
||||
renderHeaderCell(col) {
|
||||
const isActive = this.sortKey === col.key;
|
||||
const arrow = isActive ? (this.sortDir === "asc" ? " ↑" : " ↓") : "";
|
||||
|
||||
HStack(() => {
|
||||
p(col.label + arrow)
|
||||
.margin(0)
|
||||
.fontSize(0.7, em)
|
||||
.fontWeight("700")
|
||||
.letterSpacing("0.04em")
|
||||
.color("var(--headertext)")
|
||||
.opacity(isActive ? 0.75 : 0.38)
|
||||
.userSelect("none")
|
||||
.whiteSpace("nowrap")
|
||||
})
|
||||
.alignItems("center")
|
||||
.paddingHorizontal(col.key === "status" ? 0 : 0.5, em)
|
||||
.justifyContent(col.key === "status" ? "center" : "flex-start")
|
||||
.cursor(col.sortable ? "pointer" : "default")
|
||||
.onClick((done) => { if(!done) return; if (col.sortable) this.onSort(col.key) })
|
||||
}
|
||||
|
||||
renderRow(person, index) {
|
||||
const isSelected = person.id === this.selectedId;
|
||||
const isOnline = person._online;
|
||||
|
||||
HStack(() => {
|
||||
// Status dot
|
||||
HStack(() => {
|
||||
VStack(() => {})
|
||||
.width(0.5, em).height(0.5, em)
|
||||
.borderRadius(50, pct)
|
||||
.background(isOnline ? "#22c55e" : "var(--divider)")
|
||||
.flexShrink(0)
|
||||
})
|
||||
.justifyContent("center").alignItems("center")
|
||||
|
||||
// Name + avatar
|
||||
HStack(() => {
|
||||
this.renderAvatar(person, 1.85)
|
||||
VStack(() => {
|
||||
p(`${person.first_name || ""} ${person.last_name || ""}`.trim())
|
||||
.margin(0).fontSize(0.85, em).fontWeight("600")
|
||||
.color("var(--headertext)")
|
||||
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
|
||||
})
|
||||
.flex(1).minWidth(0)
|
||||
})
|
||||
.gap(0.55, em).alignItems("center").overflow("hidden").paddingHorizontal(0.5, em)
|
||||
|
||||
// Email
|
||||
this.cell(person.email, true)
|
||||
|
||||
// Phone
|
||||
this.cell(this.formatPhone(person.phone))
|
||||
|
||||
// Title
|
||||
this.cell(person.title)
|
||||
|
||||
// County
|
||||
this.cell(person.county || [person.city, person.state].filter(Boolean).join(", "))
|
||||
|
||||
// Role(s)
|
||||
HStack(() => {
|
||||
const roles = person.roles || [];
|
||||
if (roles.length === 0) {
|
||||
p("—").margin(0).fontSize(0.78, em).color("var(--headertext)").opacity(0.22)
|
||||
} else {
|
||||
roles.slice(0, 2).forEach(role => {
|
||||
p(this.capitalize(role))
|
||||
.margin(0)
|
||||
.paddingHorizontal(0.45, em).paddingVertical(0.12, em)
|
||||
.background((role === "admin" || role === "executive") ? "rgba(239,68,68,0.1)" : "var(--darkaccent)")
|
||||
.border(`1px solid ${(role === "admin" || role === "executive") ? "rgba(239,68,68,0.22)" : "var(--divider)"}`)
|
||||
.borderRadius(100, px)
|
||||
.fontSize(0.68, em).fontWeight("600")
|
||||
.color((role === "admin" || role === "executive") ? "#ef4444" : "var(--headertext)")
|
||||
.opacity((role === "admin" || role === "executive") ? 1 : 0.6)
|
||||
.whiteSpace("nowrap")
|
||||
})
|
||||
}
|
||||
})
|
||||
.gap(0.3, em).alignItems("center").paddingHorizontal(0.5, em).overflow("hidden")
|
||||
|
||||
// Tier
|
||||
HStack(() => {
|
||||
if (person.plan_name?.toLowerCase().includes("patron")) {
|
||||
p("⭐ Patron")
|
||||
.margin(0).paddingHorizontal(0.48, em).paddingVertical(0.12, em)
|
||||
.background("rgba(245,158,11,0.1)")
|
||||
.border("1px solid rgba(245,158,11,0.22)")
|
||||
.borderRadius(100, px)
|
||||
.fontSize(0.68, em).fontWeight("600").color("#d97706")
|
||||
} else if (person.plan_name?.toLowerCase().includes("annual")) {
|
||||
p("Regular")
|
||||
.margin(0).paddingHorizontal(0.48, em).paddingVertical(0.12, em)
|
||||
.background("var(--darkaccent)").border("1px solid var(--divider)")
|
||||
.borderRadius(100, px)
|
||||
.fontSize(0.68, em).color("var(--headertext)").opacity(0.5)
|
||||
} else {
|
||||
p("—").margin(0).fontSize(0.78, em).color("var(--headertext)").opacity(0.22)
|
||||
}
|
||||
})
|
||||
.alignItems("center").paddingHorizontal(0.5, em)
|
||||
|
||||
// Joined
|
||||
HStack(() => {
|
||||
p(this.formatDateShort(person.joined_network || person.created))
|
||||
.margin(0).fontSize(0.78, em).color("var(--headertext)").opacity(0.45)
|
||||
.whiteSpace("nowrap")
|
||||
})
|
||||
.alignItems("center").paddingHorizontal(0.5, em)
|
||||
|
||||
// Notes (truncated)
|
||||
HStack(() => {
|
||||
p(person.notes || "")
|
||||
.margin(0).fontSize(0.78, em)
|
||||
.color("var(--headertext)").opacity(person.notes ? 0.5 : 0.2)
|
||||
.fontStyle(person.notes ? "normal" : "italic")
|
||||
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
|
||||
.flex(1).minWidth(0)
|
||||
})
|
||||
.alignItems("center").paddingHorizontal(0.5, em).overflow("hidden").flex(1)
|
||||
})
|
||||
.attr({ style: `display: grid; grid-template-columns: ${this.gridTemplate};` })
|
||||
.paddingHorizontal(0.75, em)
|
||||
.paddingVertical(0.58, em)
|
||||
.background(isSelected
|
||||
? "var(--app)"
|
||||
: index % 2 !== 0 ? "var(--darkaccent)" : "transparent")
|
||||
.borderBottom("1px solid var(--divider)")
|
||||
.boxSizing("border-box")
|
||||
.alignItems("center")
|
||||
.onClick((done) => {if(!done) return; this.onSelect(person.id)})
|
||||
}
|
||||
|
||||
cell(value, isEmail = false) {
|
||||
HStack(() => {
|
||||
p(value || "—")
|
||||
.margin(0).fontSize(0.82, em)
|
||||
.color("var(--headertext)")
|
||||
.opacity(value ? (isEmail ? 0.6 : 0.75) : 0.22)
|
||||
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
|
||||
.flex(1).minWidth(0)
|
||||
})
|
||||
.alignItems("center").paddingHorizontal(0.5, em).overflow("hidden")
|
||||
}
|
||||
|
||||
renderAvatar(person, size) {
|
||||
if (person.image_path) {
|
||||
img(`${config.SERVER}${person.image_path}`, `${size}em`, `${size}em`)
|
||||
.borderRadius(50, pct).objectFit("cover").flexShrink(0)
|
||||
} else {
|
||||
const initials = [person.first_name?.[0], person.last_name?.[0]].filter(Boolean).join("").toUpperCase() || "?";
|
||||
VStack(() => {
|
||||
p(initials)
|
||||
.margin(0).fontSize(size * 0.38, em).fontWeight("700")
|
||||
.color("white").lineHeight("1")
|
||||
})
|
||||
.width(size, em).height(size, em).borderRadius(50, pct)
|
||||
.background(this.avatarColor(`${person.first_name} ${person.last_name}`))
|
||||
.justifyContent("center").alignItems("center").flexShrink(0)
|
||||
}
|
||||
}
|
||||
|
||||
formatPhone(phone) {
|
||||
if (!phone) return null;
|
||||
const d = phone.replace(/\D/g, "");
|
||||
if (d.length === 10) return `${d.slice(0,3)}-${d.slice(3,6)}-${d.slice(6)}`;
|
||||
return phone;
|
||||
}
|
||||
|
||||
formatDateShort(raw) {
|
||||
if (!raw) return "—";
|
||||
return new Date(raw).toLocaleDateString([], { month: "short", day: "numeric", year: "2-digit" });
|
||||
}
|
||||
|
||||
capitalize(s) {
|
||||
return s ? s[0].toUpperCase() + s.slice(1) : s;
|
||||
}
|
||||
|
||||
avatarColor(name) {
|
||||
const colors = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6", "#ec4899", "#06b6d4", "#84cc16"];
|
||||
let hash = 0;
|
||||
for (let i = 0; i < (name || "").length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
return colors[Math.abs(hash) % colors.length];
|
||||
}
|
||||
}
|
||||
|
||||
register(DesktopPeopleTable)
|
||||
Reference in New Issue
Block a user