Skip to main content

max / goingson

8.6 KB · 178 lines History Blame Raw
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 // ============ Helpers ============
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 // ============ Card Rendering ============
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 // ============ Detail Modal Rendering ============
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 // Build info section
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 // Tags
78 const tagPills = (contact.tags || []).map(t =>
79 `<span class="tag">${esc(t)}</span>`
80 ).join(' ');
81
82 // Emails
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 // Phones
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 // Social handles
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 // Custom fields
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 // ============ Populate Namespace ============
170
171 GoingsOn.contactsRender = {
172 getInitials,
173 renderCard,
174 showDetailModal,
175 };
176
177 })();
178