huge changes
This commit is contained in:
195
settings-site/public/js/discord.js
Normal file
195
settings-site/public/js/discord.js
Normal file
@@ -0,0 +1,195 @@
|
||||
// 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
|
||||
? `<span class="ss-label">${current.label}</span><span class="ss-id">${current.id}</span>`
|
||||
: `<span class="ss-placeholder">Not set</span>`;
|
||||
|
||||
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 = `<span class="ss-placeholder">Not set</span>`;
|
||||
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 += `<img class="ss-avatar" src="${opt.avatar}" alt="">`;
|
||||
if (opt.color && opt.color !== '#000000') inner += `<span class="ss-dot" style="background:${opt.color}"></span>`;
|
||||
inner += `<span class="ss-label">${opt.label}</span>`;
|
||||
if (opt.sub) inner += `<span class="ss-sub">${opt.sub}</span>`;
|
||||
item.innerHTML = inner;
|
||||
item.addEventListener('click', () => {
|
||||
inputEl.value = opt.id;
|
||||
display.innerHTML = `<span class="ss-label">${opt.label}</span><span class="ss-id">${opt.id}</span>`;
|
||||
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 = '<span class="ss-placeholder">+ Add</span>';
|
||||
|
||||
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 += `<img class="ss-avatar" src="${opt.avatar}" alt="">`;
|
||||
inner += `<span class="ss-label">${opt.label}</span>`;
|
||||
if (opt.sub) inner += `<span class="ss-sub">${opt.sub}</span>`;
|
||||
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 };
|
||||
Reference in New Issue
Block a user