fix: add org — domain no longer required, dropdown hides on blur, member bubbles UX

This commit is contained in:
James 2026-03-12 00:37:40 -04:00
parent 6e50974faf
commit 5c17bf5980
3 changed files with 118 additions and 47 deletions

View File

@ -1299,11 +1299,8 @@ func (h *Handlers) CreateOrg(w http.ResponseWriter, r *http.Request) {
ErrorResponse(w, http.StatusBadRequest, "missing_fields", "Organization name required")
return
}
if len(req.Domains) == 0 {
ErrorResponse(w, http.StatusBadRequest, "missing_fields", "At least one domain required")
return
}
// Validate domains are not empty strings
// Domains are optional — used for invite validation only
// Validate domains are not empty strings (if provided)
for _, d := range req.Domains {
if strings.TrimSpace(d) == "" {
ErrorResponse(w, http.StatusBadRequest, "invalid_domains", "Empty domain not allowed")

BIN
dealspace

Binary file not shown.

View File

@ -162,7 +162,7 @@
<div class="relative">
<label class="block text-sm font-medium text-[#94a3b8] mb-2">Organization Name</label>
<input type="text" id="addOrgName" placeholder="Start typing to search or create..." autocomplete="off"
oninput="orgNameSearch(this.value)"
oninput="orgNameSearch(this.value)" onblur="orgNameBlur()"
class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] focus:outline-none focus:border-[#c9a84c] text-sm">
<div id="orgNameDropdown" class="hidden absolute left-0 right-0 top-full mt-1 bg-[#0d1f3c] border border-white/[0.12] rounded-lg z-10 max-h-48 overflow-y-auto shadow-xl"></div>
<input type="hidden" id="addOrgId" value="">
@ -862,17 +862,16 @@
// ---- Add Org Modal ----
let orgSearchTimeout;
let memberCount = 0;
let pendingMembers = []; // array of {name, email, title}
function openAddOrgModal() {
document.getElementById('addOrgModal').classList.remove('hidden');
document.getElementById('addOrgError').classList.add('hidden');
document.getElementById('addOrgName').value = '';
document.getElementById('addOrgId').value = '';
document.getElementById('memberRows').innerHTML = '';
memberCount = 0;
pendingMembers = [];
renderMemberBubbles();
setOrgType('buyer', document.querySelector('[data-type="buyer"]'));
addMemberRow();
loadGlobalOrgs();
}
@ -904,8 +903,7 @@
function orgNameSearch(q) {
clearTimeout(orgSearchTimeout);
const dd = document.getElementById('orgNameDropdown');
const orgId = document.getElementById('addOrgId');
orgId.value = '';
document.getElementById('addOrgId').value = '';
if (!q) { dd.classList.add('hidden'); return; }
orgSearchTimeout = setTimeout(() => {
const matches = allGlobalOrgs.filter(o => {
@ -913,43 +911,122 @@
return (d.name || '').toLowerCase().includes(q.toLowerCase());
});
if (!matches.length) {
dd.innerHTML = `<div class="px-4 py-3 text-sm text-[#94a3b8]">No match — will create "<strong class="text-white">${escHtml(q)}</strong>" as new org</div>`;
} else {
// No match — just hide, the typed name will be used as-is on submit
dd.classList.add('hidden');
return;
}
dd.innerHTML = matches.map(o => {
const d = parseData(o.data_text);
const name = d.name || 'Unnamed';
return `<div onclick="selectOrg('${o.entry_id}','${escHtml(name).replace(/'/g,"\'")}','${d.role||''}',this)"
return `<div onclick="selectOrg('${o.entry_id}','${name.replace(/'/g,"\'")}','${d.role||''}')"
class="px-4 py-3 text-sm text-white hover:bg-white/[0.06] cursor-pointer flex items-center gap-3">
<span class="font-medium">${escHtml(name)}</span>
<span class="text-xs text-[#475569] capitalize">${d.role || ''}</span>
</div>`;
}).join('');
}
dd.classList.remove('hidden');
}, 200);
}
function selectOrg(orgId, orgName, role, el) {
function orgNameBlur() {
// Hide dropdown shortly after blur (allow click to register first)
setTimeout(() => document.getElementById('orgNameDropdown').classList.add('hidden'), 200);
}
function selectOrg(orgId, orgName, role) {
document.getElementById('addOrgId').value = orgId;
document.getElementById('addOrgName').value = orgName;
if (role) setOrgType(role, document.querySelector(`[data-type="${role}"]`));
document.getElementById('orgNameDropdown').classList.add('hidden');
// Load members of this org for suggestions
loadOrgMemberSuggestions(orgId);
}
function addMemberRow() {
const idx = memberCount++;
const row = document.createElement('div');
row.className = 'grid grid-cols-[1fr_1fr_1fr_auto] gap-2 items-center';
row.id = 'member-row-' + idx;
row.innerHTML = `
<input type="text" placeholder="Name" id="mname-${idx}"
let orgMemberSuggestions = [];
async function loadOrgMemberSuggestions(orgId) {
orgMemberSuggestions = [];
if (!orgId) return;
// Get members from existing deal_org entries that reference this org
try {
const res = await fetchAPI('/api/orgs/' + orgId);
const org = await res.json();
if (org && org.members) orgMemberSuggestions = org.members;
} catch(e) {}
}
// ---- Member bubble system ----
function renderMemberBubbles() {
const container = document.getElementById('memberRows');
const bubbles = pendingMembers.map((m, i) =>
`<div class="inline-flex items-center gap-1.5 px-3 py-1.5 bg-[#c9a84c]/15 border border-[#c9a84c]/30 rounded-full text-sm text-white">
<span class="w-5 h-5 rounded-full bg-[#c9a84c]/30 flex items-center justify-center text-xs font-semibold text-[#c9a84c]">${(m.name||'?')[0].toUpperCase()}</span>
<span class="font-medium">${escHtml(m.name)}</span>
${m.title ? `<span class="text-[#94a3b8] text-xs">· ${escHtml(m.title)}</span>` : ''}
<button onclick="removeMember(${i})" class="text-[#94a3b8] hover:text-red-400 transition leading-none ml-1">&times;</button>
</div>`
).join('');
container.innerHTML = `
<div class="flex flex-wrap gap-2 mb-2">${bubbles}</div>
<div class="grid grid-cols-[1fr_1fr_1fr_auto] gap-2 items-center" id="member-input-row">
<div class="relative">
<input type="text" id="new-mname" placeholder="Name" autocomplete="off"
oninput="memberNameSearch(this.value)"
class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] text-xs focus:outline-none focus:border-[#c9a84c]">
<div id="memberNameDropdown" class="hidden absolute left-0 right-0 top-full mt-1 bg-[#0d1f3c] border border-white/[0.12] rounded-lg z-20 max-h-32 overflow-y-auto shadow-xl"></div>
</div>
<input type="email" id="new-memail" placeholder="Email"
class="px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] text-xs focus:outline-none focus:border-[#c9a84c]">
<input type="email" placeholder="Email" id="memail-${idx}"
<input type="text" id="new-mtitle" placeholder="Title"
onkeydown="if(event.key==='Enter'){event.preventDefault();commitMember();}"
class="px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] text-xs focus:outline-none focus:border-[#c9a84c]">
<input type="text" placeholder="Title" id="mtitle-${idx}"
class="px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#475569] text-xs focus:outline-none focus:border-[#c9a84c]">
<button onclick="document.getElementById('member-row-${idx}').remove()" class="text-[#475569] hover:text-red-400 transition text-lg leading-none">&times;</button>`;
document.getElementById('memberRows').appendChild(row);
<button onclick="commitMember()" class="w-7 h-7 rounded-full bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] text-lg font-bold flex items-center justify-center leading-none transition">+</button>
</div>`;
}
function memberNameSearch(q) {
const dd = document.getElementById('memberNameDropdown');
if (!q || !orgMemberSuggestions.length) { dd.classList.add('hidden'); return; }
const matches = orgMemberSuggestions.filter(m =>
(m.name || '').toLowerCase().includes(q.toLowerCase()) ||
(m.email || '').toLowerCase().includes(q.toLowerCase())
);
if (!matches.length) { dd.classList.add('hidden'); return; }
dd.innerHTML = matches.map(m =>
`<div onclick="selectMemberSuggestion(${JSON.stringify(JSON.stringify(m))})"
class="px-3 py-2 text-xs text-white hover:bg-white/[0.06] cursor-pointer">
<span class="font-medium">${escHtml(m.name)}</span>
${m.email ? `<span class="text-[#475569] ml-1">${escHtml(m.email)}</span>` : ''}
</div>`
).join('');
dd.classList.remove('hidden');
}
function selectMemberSuggestion(mJson) {
const m = JSON.parse(mJson);
document.getElementById('new-mname').value = m.name || '';
document.getElementById('new-memail').value = m.email || '';
document.getElementById('new-mtitle').value = m.title || '';
document.getElementById('memberNameDropdown').classList.add('hidden');
}
function commitMember() {
const name = (document.getElementById('new-mname')?.value || '').trim();
const email = (document.getElementById('new-memail')?.value || '').trim();
const title = (document.getElementById('new-mtitle')?.value || '').trim();
if (!name && !email) return;
pendingMembers.push({ name: name || email, email, title });
// Add to org suggestions for future use
if (!orgMemberSuggestions.find(m => m.email === email)) {
orgMemberSuggestions.push({ name, email, title });
}
renderMemberBubbles();
setTimeout(() => document.getElementById('new-mname')?.focus(), 50);
}
function removeMember(idx) {
pendingMembers.splice(idx, 1);
renderMemberBubbles();
}
async function submitAddOrg() {
@ -960,15 +1037,12 @@
const btn = document.getElementById('addOrgSubmitBtn');
if (!name) { errEl.textContent = 'Organization name is required'; errEl.classList.remove('hidden'); return; }
// Collect members
const members = [];
document.querySelectorAll('#memberRows > div').forEach(row => {
const idx = row.id.replace('member-row-', '');
const n = document.getElementById('mname-' + idx)?.value.trim();
const e = document.getElementById('memail-' + idx)?.value.trim();
const t = document.getElementById('mtitle-' + idx)?.value.trim();
if (n || e) members.push({ name: n || '', email: e || '', title: t || '' });
});
// Collect members — commit any in-progress input row first
const inProgName = document.getElementById('new-mname')?.value.trim();
const inProgEmail = document.getElementById('new-memail')?.value.trim();
const inProgTitle = document.getElementById('new-mtitle')?.value.trim();
if (inProgName || inProgEmail) pendingMembers.push({ name: inProgName || inProgEmail, email: inProgEmail, title: inProgTitle });
const members = [...pendingMembers];
const perms = {
download: document.getElementById('permDownload').value,