| 1 |
|
| 2 |
* GoingsOn - Contacts Render Module |
| 3 |
* Contact card rendering, detail modal rendering |
| 4 |
|
| 5 |
|
| 6 |
(function() { |
| 7 |
'use strict'; |
| 8 |
const esc = GoingsOn.utils.escapeHtml; |
| 9 |
const escAttr = GoingsOn.utils.escapeAttr; |
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
* Extract up to 2 initials from a display name. |
| 15 |
* @param {string} name - Full display name |
| 16 |
* @returns {string} Uppercase initials (e.g. "JD") |
| 17 |
|
| 18 |
function getInitials(name) { |
| 19 |
return name.split(/\s+/).filter(Boolean).map(w => w[0]).slice(0, 2).join('').toUpperCase(); |
| 20 |
} |
| 21 |
|
| 22 |
|
| 23 |
|
| 24 |
|
| 25 |
* Render a contact card for the contacts grid. |
| 26 |
* @param {Object} c - Contact object |
| 27 |
* @returns {string} HTML string for the contact card |
| 28 |
|
| 29 |
function renderCard(c) { |
| 30 |
const initials = c.initials || getInitials(c.displayName); |
| 31 |
const primaryEmail = c.primaryEmail || c.emails?.find(e => e.isPrimary)?.address || c.emails?.[0]?.address || ''; |
| 32 |
const nickname = c.nickname ? `<span class="contact-nickname">"${esc(c.nickname)}"</span>` : ''; |
| 33 |
const company = c.company ? `<span class="contact-company">${esc(c.company)}</span>` : ''; |
| 34 |
const emailLine = primaryEmail ? `<span class="contact-email">${esc(primaryEmail)}</span>` : ''; |
| 35 |
const tagPills = (c.tags || []).map(t => |
| 36 |
`<span class="tag">${esc(t)}</span>` |
| 37 |
).join(''); |
| 38 |
|
| 39 |
return ` |
| 40 |
<div class="card contact-card" onclick="GoingsOn.contacts.open('${escAttr(c.id)}')" |
| 41 |
tabindex="0" role="button" aria-label="Open contact ${esc(c.displayName)}"> |
| 42 |
<div class="card-header contact-card-header"> |
| 43 |
<input type="checkbox" class="bulk-checkbox contact-select-cb" data-id="${escAttr(c.id)}" |
| 44 |
onclick="event.stopPropagation(); GoingsOn.contacts.toggleSelection('${escAttr(c.id)}', event)" |
| 45 |
aria-label="Select ${esc(c.displayName)}"> |
| 46 |
<div class="avatar">${esc(initials)}</div> |
| 47 |
<div class="contact-card-body"> |
| 48 |
<h3 class="card-title">${esc(c.displayName)}</h3> |
| 49 |
${nickname} |
| 50 |
${company} |
| 51 |
</div> |
| 52 |
<button class="btn btn-sm btn-secondary" onclick="event.stopPropagation(); GoingsOn.contacts.openEdit('${escAttr(c.id)}')" title="Edit">...</button> |
| 53 |
</div> |
| 54 |
${emailLine ? `<p class="card-description">${emailLine}</p>` : ''} |
| 55 |
${tagPills ? `<div class="card-meta">${tagPills}</div>` : ''} |
| 56 |
</div> |
| 57 |
`; |
| 58 |
} |
| 59 |
|
| 60 |
|
| 61 |
|
| 62 |
|
| 63 |
* Show the full contact detail modal with all sub-collections. |
| 64 |
* @param {Object} contact - Full contact object with emails, phones, socialHandles, customFields |
| 65 |
|
| 66 |
function showDetailModal(contact) { |
| 67 |
const initials = contact.initials || getInitials(contact.displayName); |
| 68 |
|
| 69 |
|
| 70 |
let info = ''; |
| 71 |
if (contact.nickname) info += `<div class="detail-row"><strong>Nickname:</strong> ${esc(contact.nickname)}</div>`; |
| 72 |
if (contact.company) info += `<div class="detail-row"><strong>Company:</strong> ${esc(contact.company)}</div>`; |
| 73 |
if (contact.title) info += `<div class="detail-row"><strong>Title:</strong> ${esc(contact.title)}</div>`; |
| 74 |
if (contact.birthday) info += `<div class="detail-row"><strong>Birthday:</strong> ${esc(contact.birthday)}</div>`; |
| 75 |
if (contact.timezone) info += `<div class="detail-row"><strong>Timezone:</strong> ${esc(contact.timezone)}</div>`; |
| 76 |
|
| 77 |
|
| 78 |
const tagPills = (contact.tags || []).map(t => |
| 79 |
`<span class="tag">${esc(t)}</span>` |
| 80 |
).join(' '); |
| 81 |
|
| 82 |
|
| 83 |
const emailRows = (contact.emails || []).map(e => ` |
| 84 |
<div class="sub-item"> |
| 85 |
<span>${esc(e.address)} ${e.label ? `<small>(${esc(e.label)})</small>` : ''} ${e.isPrimary ? '<strong>Primary</strong>' : ''}</span> |
| 86 |
<button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeEmail('${escAttr(contact.id)}', '${escAttr(e.id)}')" title="Remove">x</button> |
| 87 |
</div> |
| 88 |
`).join(''); |
| 89 |
|
| 90 |
|
| 91 |
const phoneRows = (contact.phones || []).map(p => ` |
| 92 |
<div class="sub-item"> |
| 93 |
<span>${esc(p.number)} ${p.label ? `<small>(${esc(p.label)})</small>` : ''} ${p.isPrimary ? '<strong>Primary</strong>' : ''}</span> |
| 94 |
<button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removePhone('${escAttr(contact.id)}', '${escAttr(p.id)}')" title="Remove">x</button> |
| 95 |
</div> |
| 96 |
`).join(''); |
| 97 |
|
| 98 |
|
| 99 |
const socialRows = (contact.socialHandles || []).map(s => ` |
| 100 |
<div class="sub-item"> |
| 101 |
<span><strong>${esc(s.platform)}:</strong> ${s.url ? `<a href="${escAttr(s.url)}" target="_blank">${esc(s.handle)}</a>` : esc(s.handle)}</span> |
| 102 |
<button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeSocialHandle('${escAttr(contact.id)}', '${escAttr(s.id)}')" title="Remove">x</button> |
| 103 |
</div> |
| 104 |
`).join(''); |
| 105 |
|
| 106 |
|
| 107 |
const customFieldRows = (contact.customFields || []).map(f => ` |
| 108 |
<div class="sub-item"> |
| 109 |
<span><strong>${esc(f.label)}:</strong> ${f.url ? `<a href="${escAttr(f.url)}" target="_blank">${esc(f.value)}</a>` : esc(f.value)}</span> |
| 110 |
<button class="btn btn-sm btn-danger" onclick="GoingsOn.contacts.removeCustomField('${escAttr(contact.id)}', '${escAttr(f.id)}')" title="Remove">x</button> |
| 111 |
</div> |
| 112 |
`).join(''); |
| 113 |
|
| 114 |
const content = ` |
| 115 |
<div class="contact-detail"> |
| 116 |
<div class="contact-detail-header"> |
| 117 |
<div class="avatar avatar--lg">${esc(initials)}</div> |
| 118 |
<div> |
| 119 |
<h3 class="contact-detail-name">${esc(contact.displayName)}</h3> |
| 120 |
${contact.company ? `<div class="text-secondary">${esc(contact.company)}${contact.title ? ` - ${esc(contact.title)}` : ''}</div>` : ''} |
| 121 |
</div> |
| 122 |
</div> |
| 123 |
|
| 124 |
${info ? `<div class="card card--muted contact-info-section">${info}</div>` : ''} |
| 125 |
${tagPills ? `<div class="contact-detail-tags">${tagPills}</div>` : ''} |
| 126 |
${contact.notes ? `<div class="contact-notes"><strong>Notes:</strong><p>${esc(contact.notes)}</p></div>` : ''} |
| 127 |
|
| 128 |
<div class="sub-collection"> |
| 129 |
<div class="sub-collection-header"> |
| 130 |
<h4>Email Addresses</h4> |
| 131 |
<button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddEmail('${escAttr(contact.id)}')">+ Add</button> |
| 132 |
</div> |
| 133 |
${emailRows || '<div class="sub-empty">No email addresses</div>'} |
| 134 |
</div> |
| 135 |
|
| 136 |
<div class="sub-collection"> |
| 137 |
<div class="sub-collection-header"> |
| 138 |
<h4>Phone Numbers</h4> |
| 139 |
<button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddPhone('${escAttr(contact.id)}')">+ Add</button> |
| 140 |
</div> |
| 141 |
${phoneRows || '<div class="sub-empty">No phone numbers</div>'} |
| 142 |
</div> |
| 143 |
|
| 144 |
<div class="sub-collection"> |
| 145 |
<div class="sub-collection-header"> |
| 146 |
<h4>Social Handles</h4> |
| 147 |
<button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddSocial('${escAttr(contact.id)}')">+ Add</button> |
| 148 |
</div> |
| 149 |
${socialRows || '<div class="sub-empty">No social handles</div>'} |
| 150 |
</div> |
| 151 |
|
| 152 |
<div class="sub-collection"> |
| 153 |
<div class="sub-collection-header"> |
| 154 |
<h4>Custom Fields</h4> |
| 155 |
<button class="btn btn-sm btn-primary" onclick="GoingsOn.contacts.openAddCustomField('${escAttr(contact.id)}')">+ Add</button> |
| 156 |
</div> |
| 157 |
${customFieldRows || '<div class="sub-empty">No custom fields</div>'} |
| 158 |
</div> |
| 159 |
|
| 160 |
<div class="form-actions form-actions--spaced"> |
| 161 |
<button class="btn btn-secondary" onclick="GoingsOn.contacts.openEdit('${escAttr(contact.id)}')">Edit Contact</button> |
| 162 |
</div> |
| 163 |
</div> |
| 164 |
`; |
| 165 |
|
| 166 |
GoingsOn.ui.openModal(contact.displayName, content); |
| 167 |
} |
| 168 |
|
| 169 |
|
| 170 |
|
| 171 |
GoingsOn.contactsRender = { |
| 172 |
getInitials, |
| 173 |
renderCard, |
| 174 |
showDetailModal, |
| 175 |
}; |
| 176 |
|
| 177 |
})(); |
| 178 |
|