311 lines
14 KiB
JavaScript
311 lines
14 KiB
JavaScript
import server from "/people/@server/index.js"
|
|
|
|
class DesktopPeopleDetail extends Shadow {
|
|
constructor(person, onSaved) {
|
|
super()
|
|
this.person = person
|
|
this.onSaved = onSaved
|
|
this.editingNotes = false
|
|
this.notesDraft = ""
|
|
this.notesSaving = false
|
|
}
|
|
|
|
render() {
|
|
if (!this.person) {
|
|
VStack(() => {
|
|
p("👤")
|
|
.margin(0).fontSize(2.8, em).opacity(0.15)
|
|
p("Select a member to view their profile")
|
|
.margin(0).marginTop(0.75, em).fontSize(0.9, em)
|
|
.color("var(--headertext)").opacity(0.32).textAlign("center")
|
|
})
|
|
.flex(1).height(100, pct).justifyContent("center").alignItems("center")
|
|
return;
|
|
}
|
|
|
|
const p_ = this.person;
|
|
const isOnline = p_._online;
|
|
const isAdmin = (p_.roles || []).includes("admin");
|
|
|
|
VStack(() => {
|
|
// ── Profile header ────────────────────────────────────────
|
|
VStack(() => {
|
|
HStack(() => {
|
|
// Large avatar
|
|
ZStack(() => {
|
|
this.renderAvatar(p_, 5)
|
|
VStack(() => {})
|
|
.width(0.9, em).height(0.9, em).borderRadius(50, pct)
|
|
.background(isOnline ? "#22c55e" : "var(--divider)")
|
|
.boxSizing("border-box")
|
|
.position("absolute").bottom(0.1, em).right(0.1, em)
|
|
})
|
|
.position("relative").flexShrink(0)
|
|
|
|
VStack(() => {
|
|
HStack(() => {
|
|
h2(`${p_.first_name} ${p_.last_name}`)
|
|
.margin(0).fontSize(1.3, em).fontWeight("700")
|
|
.color("var(--headertext)").lineHeight("1.2")
|
|
|
|
HStack(() => {
|
|
if (isOnline) {
|
|
p("● Online")
|
|
.margin(0).fontSize(0.72, em).fontWeight("600").color("#22c55e")
|
|
} else {
|
|
p("○ Offline")
|
|
.margin(0).fontSize(0.72, em).color("var(--headertext)").opacity(0.35)
|
|
}
|
|
})
|
|
})
|
|
.gap(0.75, em).alignItems("center")
|
|
|
|
if (p_.title) {
|
|
p(p_.title)
|
|
.margin(0).marginTop(0.22, em).fontSize(0.9, em)
|
|
.color("var(--headertext)").opacity(0.55).fontWeight("400")
|
|
}
|
|
|
|
// Role + tier badges
|
|
HStack(() => {
|
|
(p_.roles || []).forEach(role => {
|
|
p(this.capitalize(role))
|
|
.margin(0).paddingHorizontal(0.6, em).paddingVertical(0.2, em)
|
|
.background(role === "admin" ? "rgba(239,68,68,0.12)" : "var(--darkaccent)")
|
|
.border(`1px solid ${role === "admin" ? "rgba(239,68,68,0.25)" : "var(--divider)"}`)
|
|
.borderRadius(100, px)
|
|
.fontSize(0.72, em).fontWeight("600")
|
|
.color(role === "admin" ? "#ef4444" : "var(--headertext)")
|
|
.opacity(role === "admin" ? 1 : 0.65)
|
|
})
|
|
if (p_.plan_name?.includes("Patron")) {
|
|
p("⭐ Patron")
|
|
.margin(0).paddingHorizontal(0.6, em).paddingVertical(0.2, em)
|
|
.background("rgba(245,158,11,0.1)")
|
|
.border("1px solid rgba(245,158,11,0.25)")
|
|
.borderRadius(100, px)
|
|
.fontSize(0.72, em).fontWeight("600").color("#d97706")
|
|
} else if (p_.plan_name?.includes("Annual")) {
|
|
p("Regular")
|
|
.margin(0).paddingHorizontal(0.6, em).paddingVertical(0.2, em)
|
|
.background("var(--darkaccent)").border("1px solid var(--divider)")
|
|
.borderRadius(100, px)
|
|
.fontSize(0.72, em).fontWeight("500")
|
|
.color("var(--headertext)").opacity(0.55)
|
|
}
|
|
})
|
|
.gap(0.4, em).flexWrap("wrap").marginTop(0.65, em)
|
|
})
|
|
.flex(1).minWidth(0)
|
|
})
|
|
.gap(1.25, em).alignItems("flex-start")
|
|
})
|
|
.paddingHorizontal(1.75, em).paddingTop(1.6, em).paddingBottom(1.35, em)
|
|
.borderBottom("1px solid var(--divider)").flexShrink(0)
|
|
|
|
// ── Body ──────────────────────────────────────────────────
|
|
VStack(() => {
|
|
// Contact
|
|
this.section("Contact", () => {
|
|
this.infoRow("✉", "Email", p_.email)
|
|
if (p_.phone) this.infoRow("📞", "Phone", this.formatPhone(p_.phone))
|
|
})
|
|
|
|
// Location
|
|
if (p_.city || p_.state || p_.county) {
|
|
this.section("Location", () => {
|
|
if (p_.city || p_.state) this.infoRow("📍", "City / State", [p_.city, p_.state].filter(Boolean).join(", "))
|
|
if (p_.county) this.infoRow("🗺", "County", p_.county)
|
|
})
|
|
}
|
|
|
|
// Bio
|
|
if (p_.bio) {
|
|
this.section("Bio", () => {
|
|
p(p_.bio)
|
|
.margin(0).fontSize(0.88, em)
|
|
.color("var(--headertext)").opacity(0.75)
|
|
.lineHeight("1.6").whiteSpace("pre-wrap")
|
|
})
|
|
}
|
|
|
|
// Membership
|
|
this.section("Membership", () => {
|
|
this.infoRow("📅", "Joined", this.formatDate(p_.joined_network || p_.created))
|
|
if (p_.subscription_status) this.infoRow("💳", "Status", this.capitalize(p_.subscription_status))
|
|
})
|
|
|
|
// Notes (editable)
|
|
this.section("Notes", () => {
|
|
if (this.editingNotes) {
|
|
VStack(() => {
|
|
textarea(p_.notes || "")
|
|
.attr({ rows: 5, id: "notes-textarea" })
|
|
.width(100, pct)
|
|
.padding(0.65, em)
|
|
.background("var(--darkaccent)")
|
|
.border("1px solid var(--divider)")
|
|
.borderRadius(0.45, em)
|
|
.color("var(--headertext)")
|
|
.fontSize(0.88, em)
|
|
.lineHeight("1.6")
|
|
.outline("none")
|
|
.resize("vertical")
|
|
.boxSizing("border-box")
|
|
.onInput((e) => { this.notesDraft = e.target.value; })
|
|
.onAppear(function() { this.value = this.placeholder; } )
|
|
|
|
HStack(() => {
|
|
button("Cancel")
|
|
.paddingHorizontal(0.9, em).paddingVertical(0.4, em)
|
|
.background("transparent")
|
|
.border("1px solid var(--divider)")
|
|
.borderRadius(0.4, em)
|
|
.color("var(--headertext)").fontSize(0.82, em)
|
|
.cursor("pointer").opacity(0.65)
|
|
.onClick((done) => {if(!done) return; this.editingNotes = false; this.notesDraft = ""; this.rerender(); })
|
|
|
|
button(this.notesSaving ? "Saving…" : "Save")
|
|
.attr({ type: "button" })
|
|
.paddingHorizontal(0.9, em).paddingVertical(0.4, em)
|
|
.background("var(--quillred)").border("none")
|
|
.borderRadius(0.4, em)
|
|
.color("white").fontSize(0.82, em).fontWeight("600")
|
|
.cursor("pointer")
|
|
.onClick((done) => { if (!done) return; this.saveNotes() })
|
|
})
|
|
.gap(0.5, em).marginTop(0.65, em).justifyContent("flex-end")
|
|
})
|
|
.width(100, pct)
|
|
} else {
|
|
VStack(() => {
|
|
if (p_.notes) {
|
|
p(p_.notes)
|
|
.margin(0).fontSize(0.88, em)
|
|
.color("var(--headertext)").opacity(0.75)
|
|
.lineHeight("1.6").whiteSpace("pre-wrap")
|
|
} else {
|
|
p("No notes yet. Click Edit to add one.")
|
|
.margin(0).fontSize(0.85, em)
|
|
.color("var(--headertext)").opacity(0.3)
|
|
.fontStyle("italic")
|
|
}
|
|
|
|
button("Edit notes")
|
|
.marginTop(0.65, em)
|
|
.paddingHorizontal(0.9, em).paddingVertical(0.38, em)
|
|
.background("transparent").border("1px solid var(--divider)")
|
|
.borderRadius(0.4, em).color("var(--headertext)")
|
|
.fontSize(0.8, em).cursor("pointer").opacity(0.6)
|
|
.onClick((done) => {if(!done) return;
|
|
this.notesDraft = p_.notes || "";
|
|
this.editingNotes = true;
|
|
this.rerender();
|
|
})
|
|
})
|
|
.width(100, pct)
|
|
}
|
|
})
|
|
})
|
|
.paddingHorizontal(1.75, em).paddingTop(0).paddingBottom(2, em)
|
|
.overflowY("auto").flex(1).gap(0)
|
|
})
|
|
.height(100, pct).width(100, pct).overflow("hidden").boxSizing("border-box")
|
|
}
|
|
|
|
section(title, contentFn) {
|
|
VStack(() => {
|
|
p(title.toUpperCase())
|
|
.margin(0).marginBottom(0.7, em)
|
|
.fontSize(0.62, em).fontWeight("700").letterSpacing("0.07em")
|
|
.color("var(--headertext)").opacity(0.35)
|
|
contentFn()
|
|
})
|
|
.paddingTop(1.25, em).paddingBottom(0.5, em)
|
|
.borderBottom("1px solid var(--divider)")
|
|
.width(100, pct).boxSizing("border-box")
|
|
}
|
|
|
|
infoRow(icon, label, value) {
|
|
HStack(() => {
|
|
p(icon)
|
|
.margin(0).width(1.1, em).fontSize(0.85, em)
|
|
.textAlign("center").flexShrink(0).opacity(0.6)
|
|
p(label)
|
|
.margin(0).fontSize(0.82, em)
|
|
.color("var(--headertext)").opacity(0.42)
|
|
.width(6.5, em).flexShrink(0)
|
|
p(value)
|
|
.margin(0).fontSize(0.85, em)
|
|
.color("var(--headertext)").fontWeight("500")
|
|
.flex(1).minWidth(0)
|
|
.overflow("hidden").whiteSpace("nowrap").textOverflow("ellipsis")
|
|
})
|
|
.gap(0.55, em).paddingVertical(0.42, em)
|
|
.borderBottom("1px solid var(--divider)")
|
|
.alignItems("center").width(100, pct)
|
|
}
|
|
|
|
renderAvatar(person, size) {
|
|
if (person.image_path) {
|
|
img(`${config.UI}${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.32, 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)
|
|
}
|
|
}
|
|
|
|
async saveNotes() {
|
|
if (this.notesSaving) return;
|
|
this.notesSaving = true;
|
|
|
|
const result = await server.saveMemberNote(this.person.email, this.notesDraft);
|
|
if (result?.success) {
|
|
this.person.notes = this.notesDraft;
|
|
this.notesSaving = false;
|
|
this.editingNotes = false;
|
|
this.notesDraft = "";
|
|
this.onSaved(this.person);
|
|
} else {
|
|
this.notesSaving = false;
|
|
this.editingNotes = false;
|
|
this.notesDraft = "";
|
|
this.rerender();
|
|
}
|
|
}
|
|
|
|
formatPhone(phone) {
|
|
const d = phone.replace(/\D/g, "");
|
|
if (d.length === 10) return `${d.slice(0,3)}-${d.slice(3,6)}-${d.slice(6)}`;
|
|
if (d.length === 11) return `${d.slice(0,1)}-${d.slice(1,4)}-${d.slice(4,7)}-${d.slice(7)}`;
|
|
return phone;
|
|
}
|
|
|
|
formatDate(raw) {
|
|
if (!raw) return "—";
|
|
const d = new Date(raw);
|
|
return d.toLocaleDateString(undefined, { month: "long", day: "numeric", year: "numeric" });
|
|
}
|
|
|
|
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(DesktopPeopleDetail)
|