// Discord guild data cache let guildData = null; let guildDataPromise = null; async function fetchGuildData() { if (guildData) return guildData; if (guildDataPromise) return guildDataPromise; guildDataPromise = fetch('/api/discord/guild') .then(r => r.json()) .then(data => { guildData = data; return data; }) .catch(() => ({ channels: [], roles: [], members: [], categories: [] })); return guildDataPromise; } async function renderChannelSelect(el, currentValue, filter) { const data = await fetchGuildData(); const channels = filter ? data.channels.filter(filter) : data.channels; renderSmartSelect(el, channels.map(c => ({ id: c.id, label: `#${c.name}`, sub: c.parentId ? (data.categories.find(cat => cat.id === c.parentId)?.name || null) : null })), currentValue); } async function renderCategorySelect(el, currentValue) { const data = await fetchGuildData(); renderSmartSelect(el, data.categories.map(c => ({ id: c.id, label: c.name })), currentValue); } async function renderRoleSelect(el, currentValue) { const data = await fetchGuildData(); renderSmartSelect(el, data.roles.map(r => ({ id: r.id, label: `@${r.name}`, color: r.color })), currentValue); } async function renderMemberSelect(el, currentValue) { const data = await fetchGuildData(); renderSmartSelect(el, data.members.map(m => ({ id: m.id, label: m.displayName, sub: `@${m.username}`, avatar: m.avatar })), currentValue); } async function renderMultiMemberSelect(el, currentValue) { const data = await fetchGuildData(); const currentIds = (currentValue || '').split(',').map(s => s.trim()).filter(Boolean); renderMultiSelect(el, data.members.map(m => ({ id: m.id, label: m.displayName, sub: `@${m.username}`, avatar: m.avatar })), currentIds); } function renderSmartSelect(inputEl, options, currentValue) { const wrapper = document.createElement('div'); wrapper.className = 'smart-select'; const current = options.find(o => o.id === currentValue); const display = document.createElement('div'); display.className = 'smart-select-display'; display.innerHTML = current ? `${current.label}${current.id}` : `Not set`; const dropdown = document.createElement('div'); dropdown.className = 'smart-select-dropdown hidden'; const search = document.createElement('input'); search.type = 'text'; search.placeholder = 'Search...'; search.className = 'ss-search'; const list = document.createElement('div'); list.className = 'ss-list'; const clearOpt = document.createElement('div'); clearOpt.className = 'ss-option ss-clear'; clearOpt.textContent = 'Clear (not set)'; clearOpt.addEventListener('click', () => { inputEl.value = ''; display.innerHTML = `Not set`; dropdown.classList.add('hidden'); inputEl.dispatchEvent(new Event('change')); }); list.appendChild(clearOpt); function renderOptions(filter = '') { while (list.children.length > 1) list.removeChild(list.lastChild); const filtered = filter ? options.filter(o => o.label.toLowerCase().includes(filter.toLowerCase()) || (o.sub || '').toLowerCase().includes(filter.toLowerCase()) || o.id.includes(filter)) : options; for (const opt of filtered.slice(0, 50)) { const item = document.createElement('div'); item.className = 'ss-option' + (opt.id === inputEl.value ? ' selected' : ''); let inner = ''; if (opt.avatar) inner += ``; if (opt.color && opt.color !== '#000000') inner += ``; inner += `${opt.label}`; if (opt.sub) inner += `${opt.sub}`; item.innerHTML = inner; item.addEventListener('click', () => { inputEl.value = opt.id; display.innerHTML = `${opt.label}${opt.id}`; dropdown.classList.add('hidden'); inputEl.dispatchEvent(new Event('change')); }); list.appendChild(item); } } renderOptions(); search.addEventListener('input', () => renderOptions(search.value)); display.addEventListener('click', () => { dropdown.classList.toggle('hidden'); if (!dropdown.classList.contains('hidden')) search.focus(); }); document.addEventListener('click', (e) => { if (!wrapper.contains(e.target)) dropdown.classList.add('hidden'); }); dropdown.appendChild(search); dropdown.appendChild(list); wrapper.appendChild(display); wrapper.appendChild(dropdown); inputEl.style.display = 'none'; inputEl.parentNode.insertBefore(wrapper, inputEl.nextSibling); } function renderMultiSelect(inputEl, options, currentIds) { const wrapper = document.createElement('div'); wrapper.className = 'smart-select'; const selected = new Set(currentIds); function updateInput() { inputEl.value = [...selected].join(','); inputEl.dispatchEvent(new Event('change')); } function renderChips() { chipsEl.innerHTML = ''; for (const id of selected) { const opt = options.find(o => o.id === id); const chip = document.createElement('span'); chip.className = 'ss-option selected'; chip.style.cssText = 'display:inline-flex;padding:4px 8px;margin:2px;border-radius:12px;font-size:12px;cursor:pointer;'; chip.textContent = opt ? opt.label : id; chip.title = 'Click to remove'; chip.addEventListener('click', () => { selected.delete(id); renderChips(); updateInput(); }); chipsEl.appendChild(chip); } } const chipsEl = document.createElement('div'); chipsEl.style.cssText = 'display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px;'; renderChips(); const addBtn = document.createElement('div'); addBtn.className = 'smart-select-display'; addBtn.innerHTML = '+ Add'; const dropdown = document.createElement('div'); dropdown.className = 'smart-select-dropdown hidden'; const search = document.createElement('input'); search.type = 'text'; search.placeholder = 'Search...'; search.className = 'ss-search'; const list = document.createElement('div'); list.className = 'ss-list'; function renderOptions(filter = '') { list.innerHTML = ''; const filtered = filter ? options.filter(o => !selected.has(o.id) && (o.label.toLowerCase().includes(filter.toLowerCase()) || o.id.includes(filter))) : options.filter(o => !selected.has(o.id)); for (const opt of filtered.slice(0, 50)) { const item = document.createElement('div'); item.className = 'ss-option'; let inner = ''; if (opt.avatar) inner += ``; inner += `${opt.label}`; if (opt.sub) inner += `${opt.sub}`; item.innerHTML = inner; item.addEventListener('click', () => { selected.add(opt.id); renderChips(); renderOptions(search.value); updateInput(); }); list.appendChild(item); } } renderOptions(); search.addEventListener('input', () => renderOptions(search.value)); addBtn.addEventListener('click', () => { dropdown.classList.toggle('hidden'); if (!dropdown.classList.contains('hidden')) search.focus(); }); document.addEventListener('click', (e) => { if (!wrapper.contains(e.target)) dropdown.classList.add('hidden'); }); dropdown.appendChild(search); dropdown.appendChild(list); wrapper.appendChild(chipsEl); wrapper.appendChild(addBtn); wrapper.appendChild(dropdown); inputEl.style.display = 'none'; inputEl.parentNode.insertBefore(wrapper, inputEl.nextSibling); } window.DiscordFields = { fetchGuildData, renderChannelSelect, renderCategorySelect, renderRoleSelect, renderMemberSelect, renderMultiMemberSelect };