1const GUILD_WIDGET_URL = 'https://discord.com/api/guilds/1043125489264513085/widget.json'; 2const REFRESH_MS = 30_000; 3 4function escapeHtml(value) { 5 return String(value) 6 .replace(/&/g, '&') 7 .replace(/</g, '<') 8 .replace(/>/g, '>') 9 .replace(/"/g, '"') 10 .replace(/'/g, '''); 11} 12 13function safeAvatarUrl(url) { 14 if (typeof url !== 'string' || !url.startsWith('https://')) return ''; 15 return url; 16} 17 18function statusColor(status) { 19 if (status === 'online') return '#43b581'; 20 if (status === 'idle') return '#faa61a'; 21 if (status === 'dnd') return '#f04747'; 22 return '#747f8d'; 23} 24 25function shuffleMembers(members, existingUsernames) { 26 const copy = members.slice(); 27 if (!existingUsernames.length) { 28 copy.sort(() => Math.random() - 0.5); 29 return copy; 30 } 31 copy.sort((a, b) => { 32 const aKnown = existingUsernames.includes(a.username); 33 const bKnown = existingUsernames.includes(b.username); 34 if (aKnown && !bKnown) return -1; 35 if (!aKnown && bKnown) return 1; 36 return Math.random() - 0.5; 37 }); 38 return copy; 39} 40 41function renderWidget(root, data, previousCount) { 42 const onlineCount = Number(data.presence_count) || 0; 43 const members = Array.isArray(data.members) ? data.members : []; 44 const onlineMembers = members.filter((m) => ['online', 'idle', 'dnd'].includes(m.status)); 45 const existing = root.querySelector('.discord-card'); 46 const existingUsernames = existing 47 ? [...existing.querySelectorAll('.discord-card__avatar')].map((el) => el.getAttribute('title') || '') 48 : []; 49 50 const preview = shuffleMembers(onlineMembers, existingUsernames).slice(0, 20); 51 const countClass = previousCount > 0 && previousCount !== onlineCount ? ' is-updated' : ''; 52 53 let avatars = ''; 54 preview.forEach((member) => { 55 const color = statusColor(member.status); 56 const username = escapeHtml(member.username || 'User'); 57 const avatarUrl = safeAvatarUrl(member.avatar_url); 58 avatars += `<div class="discord-card__avatar" style="background-image:url('${avatarUrl}')" title="${username}"><span class="discord-card__status" style="background-color:${color}"></span></div>`; 59 }); 60 if (onlineMembers.length > preview.length) { 61 avatars += `<div class="discord-card__more">+${onlineMembers.length - preview.length}</div>`; 62 } 63 64 root.innerHTML = ` 65 <div class="discord-card"> 66 <div class="discord-card__header"> 67 <span class="discord-card__live" aria-hidden="true"></span> 68 <p class="discord-card__name">${escapeHtml(data.name || 'The Disniverse')}</p> 69 <div class="discord-card__stat"> 70 <span class="discord-card__count${countClass}">${onlineCount}</span> 71 <span class="discord-card__label">online</span> 72 </div> 73 </div> 74 ${preview.length ? `<div class="discord-card__avatars" aria-hidden="true">${avatars}</div>` : ''} 75 </div>`; 76 77 const countEl = root.querySelector('.discord-card__count'); 78 if (countEl && countClass) { 79 window.setTimeout(() => countEl.classList.remove('is-updated'), 500); 80 } 81 82 return onlineCount; 83} 84 85export function initDiscordWidget() { 86 const root = document.getElementById('discordWidget'); 87 if (!root) return null; 88 89 let previousCount = 0; 90 let timerId = 0; 91 let abortController = null; 92 93 const showError = () => { 94 root.innerHTML = ` 95 <div class="discord-card__error"> 96 <p>Unable to load Discord widget</p> 97 <a href="https://discord.gg/YdMes7g6" class="btn btn--primary btn--sm discord-invite" target="_blank" rel="noopener noreferrer">Join Discord</a> 98 </div>`; 99 if (typeof window.applyDiscordInvites === 'function') { 100 window.applyDiscordInvites(root); 101 } 102 }; 103 104 const refresh = async () => { 105 abortController?.abort(); 106 abortController = new AbortController(); 107 108 try { 109 const response = await fetch(GUILD_WIDGET_URL, { signal: abortController.signal }); 110 if (!response.ok) throw new Error('widget fetch failed'); 111 const data = await response.json(); 112 previousCount = renderWidget(root, data, previousCount); 113 if (typeof window.applyDiscordInvites === 'function') { 114 window.applyDiscordInvites(root); 115 } 116 } catch (error) { 117 if (error.name !== 'AbortError') showError(); 118 } 119 }; 120 121 refresh(); 122 timerId = window.setInterval(refresh, REFRESH_MS); 123 124 document.addEventListener('visibilitychange', () => { 125 if (document.hidden) { 126 window.clearInterval(timerId); 127 abortController?.abort(); 128 } else { 129 refresh(); 130 timerId = window.setInterval(refresh, REFRESH_MS); 131 } 132 }); 133 134 return () => { 135 window.clearInterval(timerId); 136 abortController?.abort(); 137 }; 138}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.