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)