1/* ========================================================================== 2 GLOBAL HOTEL MAP - COMMUNITY FEATURES (members, events, leaderboard, chat) 3 ========================================================================== */ 4 5(function () { 6 const API = '/maps/api'; 7 8 let map = null; 9 let memberLayer = null; 10 let eventLayer = null; 11 const memberMarkers = []; 12 const eventMarkers = []; 13 14 function init(_map) { 15 map = _map; 16 memberLayer = L.layerGroup().addTo(map); 17 eventLayer = L.layerGroup().addTo(map); 18 bindUI(); 19 loadAll(); 20 setInterval(loadMembers, 60000); 21 setInterval(loadEvents, 60000); 22 } 23 24 window.addEventListener('aimap:ready', (e) => { 25 if (!map) init(e.detail.map); 26 }); 27 if (window.__aiMap && !map) init(window.__aiMap.map); 28 29 // ============ API helpers ============ 30 async function api(path, opts) { 31 const res = await fetch(API + path, opts); 32 if (!res.ok) throw new Error('HTTP ' + res.status); 33 return res.json(); 34 } 35 36 const $ = (id) => document.getElementById(id); 37 38 // ============ Member markers ============ 39 function clearMemberMarkers() { 40 memberLayer.clearLayers(); 41 memberMarkers.length = 0; 42 } 43 44 async function loadMembers() { 45 try { 46 const members = await api('/members?limit=500'); 47 clearMemberMarkers(); 48 members.forEach((m) => { 49 if (!m.lat || !m.lng) return; 50 const icon = L.divIcon({ 51 className: '', 52 html: `<div class="member-marker" style="background:${m.avatar_color || '#00f0ff'};width:22px;height:22px;">${(m.name || '?').charAt(0).toUpperCase()}</div>`, 53 iconSize: [22, 22], 54 iconAnchor: [11, 11] 55 }); 56 const marker = L.marker([m.lat, m.lng], { icon }).addTo(memberLayer); 57 marker.bindTooltip( 58 `<div class="tooltip-title" style="color:#00f0ff;">ð¤ ${m.name}</div> 59 <div class="tooltip-stat"><span>Role:</span> <span>${m.role || 'Traveller'}</span></div> 60 <div class="tooltip-stat"><span>Lokasi:</span> <span>${m.city || ''}${m.city && m.country ? ', ' : ''}${m.country || ''}</span></div> 61 ${m.is_verified ? '<div class="tooltip-stat"><span>Badge:</span> <span style="color:#ffe600;">â VERIFIED</span></div>' : ''}`, 62 { className: 'cyber-tooltip', direction: 'top', offset: [0, -12] } 63 ); 64 marker.on('click', () => {
65 map.setView([m.lat, m.lng], 8); 66 }); 67 memberMarkers.push(marker); 68 }); 69 } catch (err) { 70 console.warn('loadMembers:', err); 71 } 72 } 73 74 // ============ Event markers ============ 75 function clearEventMarkers() { 76 eventLayer.clearLayers(); 77 eventMarkers.length = 0; 78 } 79 80 async function loadEvents() { 81 try { 82 const events = await api('/events'); 83 clearEventMarkers(); 84 events.forEach((ev) => { 85 if (!ev.lat || !ev.lng) return; 86 const icon = L.divIcon({ 87 className: '', 88 html: `<div class="event-marker" style="background:#ffe600;width:26px;height:26px;border-radius:50%;">ð</div>`, 89 iconSize: [26, 26], 90 iconAnchor: [13, 13] 91 }); 92 const marker = L.marker([ev.lat, ev.lng], { icon }).addTo(eventLayer); 93 const timeStr = ev.start_time ? new Date(ev.start_time).toLocaleString() : ''; 94 marker.bindTooltip( 95 `<div class="tooltip-title" style="color:#ffe600;">ð ${ev.title}</div> 96 <div class="tooltip-stat"><span>Type:</span> <span>${ev.event_type || 'Meetup'}</span></div> 97 <div class="tooltip-stat"><span>Lokasi:</span> <span>${ev.city || ''}${ev.city && ev.country ? ', ' : ''}${ev.country || ''}</span></div> 98 ${timeStr ? `<div class="tooltip-stat"><span>Waktu:</span> <span>${timeStr}</span></div>` : ''} 99 ${ev.join_url ? `<div class="tooltip-stat"><span>Join:</span> <span><a href="${ev.join_url}" target="_blank" style="color:#00f0ff;">Daftar →</a></span></div>` : ''}`, 100 { className: 'cyber-tooltip', direction: 'top', offset: [0, -12] } 101 ); 102 marker.on('click', () => {
103 map.setView([ev.lat, ev.lng], 8); 104 renderEvents(); 105 }); 106 eventMarkers.push(marker); 107 }); 108 } catch (err) { 109 console.warn('loadEvents:', err); 110 } 111 } 112 113 // ============ Panel rendering ============ 114 function openPanel(title) { 115 $('community-panel-title').textContent = '> ' + title; 116 $('community-panel').classList.add('open'); 117 } 118 119 function closePanel() { 120 $('community-panel').classList.remove('open'); 121 } 122 123 async function renderLeaderboard() { 124 openPanel('LEADERBOARD'); 125 const body = $('community-panel-body'); 126 body.innerHTML = '<div class="panel-empty">Memuat leaderboard...</div>'; 127 try { 128 const data = await api('/leaderboard'); 129 let html = '<div class="community-section-title">TOP KOTA / CITIES</div>'; 130 if (!data.cities.length) { 131 html += '<div class="panel-empty">Belum ada member. Jadilah yang pertama!</div>'; 132 } else { 133 data.cities.forEach((c, i) => { 134 const medal = i === 0 ? 'ð¥' : i === 1 ? 'ð¥' : i === 2 ? 'ð¥' : ''; 135 html += `<div class="leaderboard-row"><span class="leaderboard-pos">${medal || (i + 1)}</span><span class="leaderboard-name">${c.city} <span style="color:#64748b;">· ${c.country}</span></span><span class="leaderboard-val">${c.members}</span></div>`; 136 }); 137 } 138 html += '<div class="community-section-title">TOP NEGARA / COUNTRIES</div>'; 139 data.countries.forEach((c, i) => { 140 const medal = i === 0 ? 'ð¥' : i === 1 ? 'ð¥' : i === 2 ? 'ð¥' : ''; 141 html += `<div class="leaderboard-row"><span class="leaderboard-pos">${medal || (i + 1)}</span><span class="leaderboard-name">${c.country}</span><span class="leaderboard-val">${c.members}</span></div>`; 142 }); 143 html += '<div class="community-section-title">MEMBER TERBARU</div>'; 144 data.recent.forEach((m) => { 145 html += `<div class="community-item"><div class="community-avatar" style="background:${m.avatar_color || '#00f0ff'};">${(m.name || '?').charAt(0).toUpperCase()}</div><div><div class="community-item-name">${m.name}${m.is_verified ? '<span class="verified-badge">VERIFIED</span>' : ''}</div><div class="community-item-meta">${m.role || 'Traveller'} · ${m.city || ''} ${m.country || ''}</div></div></div>`; 146 }); 147 body.innerHTML = html; 148 } catch (err) { 149 body.innerHTML = `<div class="panel-empty">Gagal memuat: ${err.message}</div>`; 150 } 151 } 152 153 async function renderMembers() { 154 openPanel('GLOBAL MEMBERS'); 155 const body = $('community-panel-body'); 156 body.innerHTML = '<div class="panel-empty">Memuat member...</div>'; 157 try { 158 const members = await api('/members?limit=200'); 159 if (!members.length) { 160 body.innerHTML = '<div class="panel-empty">Belum ada member global. Klik "JOIN GLOBAL NETWORK" untuk menjadi yang pertama!</div>'; 161 return; 162 } 163 let html = `<div class="community-section-title">${members.length} MEMBERS TERDAFTAR</div>`; 164 members.forEach((m) => { 165 html += `<div class="community-item"><div class="community-avatar" style="background:${m.avatar_color || '#00f0ff'};">${(m.name || '?').charAt(0).toUpperCase()}</div><div><div class="community-item-name">${m.name}${m.is_verified ? '<span class="verified-badge">VERIFIED</span>' : ''}</div><div class="community-item-meta">${m.role || 'Traveller'} · ${m.city || ''}${m.city && m.country ? ', ' : ''}${m.country || ''}</div></div></div>`; 166 }); 167 body.innerHTML = html; 168 } catch (err) { 169 body.innerHTML = `<div class="panel-empty">Gagal memuat: ${err.message}</div>`; 170 } 171 } 172 173 async function renderEvents() { 174 openPanel('GLOBAL EVENTS'); 175 const body = $('community-panel-body'); 176 body.innerHTML = '<div class="panel-empty">Memuat event...</div>'; 177 try { 178 const events = await api('/events'); 179 let html = '<div style="display:flex;gap:8px;margin-bottom:10px;"><button class="btn-panel yellow" id="btn-new-event">+ POST EVENT</button></div>'; 180 if (!events.length) { 181 html += '<div class="panel-empty">Belum ada event global. Post event/meetup pertamamu!</div>'; 182 } else {
183 events.slice(0, 30).forEach((ev) => { 184 const timeStr = ev.start_time ? new Date(ev.start_time).toLocaleString() : ''; 185 html += `<div class="event-item"><div class="event-item-head"><div class="event-item-title">ð ${ev.title}</div><button class="btn-event-del" data-event-id="${ev.id}" title="Hapus event (admin)">DEL</button></div><div class="event-item-meta">${ev.event_type || 'Meetup'} · ${ev.city || ''}${ev.city && ev.country ? ', ' : ''}${ev.country || ''}</div>${timeStr ? `<div class="event-item-meta">ð ${timeStr}</div>` : ''}${ev.description ? `<div class="event-item-desc">${ev.description}</div>` : ''}${ev.join_url ? `<div class="event-item-link"><a href="${ev.join_url}" target="_blank">Join event →</a></div>` : ''}</div>`; 186 }); 187 } 188 body.innerHTML = html; 189 $('btn-new-event').addEventListener('click', openEventModal); 190 body.querySelectorAll('.btn-event-del').forEach((btn) => { 191 btn.addEventListener('click', (e) => { 192 e.preventDefault(); 193 e.stopPropagation(); 194 deleteEvent(parseInt(btn.getAttribute('data-event-id')), btn); 195 }); 196 }); 197 } catch (err) { 198 body.innerHTML = `<div class="panel-empty">Gagal memuat: ${err.message}</div>`; 199 } 200 } 201 202 async function renderChat() { 203 openPanel('GLOBAL CHAT'); 204 const body = $('community-panel-body'); 205 const countryFilter = window.__chatCountryFilter || ''; 206 let html = ` 207 <div class="chat-filter"> 208 <input type="text" id="chat-filter-input" class="join-input" placeholder="Filter by country..." value="${countryFilter}"> 209 <button class="btn-panel" id="btn-apply-filter">FILTER</button> 210 </div> 211 <div id="chat-panel-messages" class="chat-messages" style="max-height:220px;"><div class="chat-placeholder">Memuat...</div></div> 212 <div class="chat-compose"> 213 <input type="text" id="chat-name-input" class="join-input" placeholder="Your name" maxlength="40"> 214 <input type="text" id="chat-message-input" class="join-input" placeholder="Type message..." maxlength="280"> 215 <button class="btn-join" id="btn-send-msg" style="align-self:flex-start;padding:4px 12px;font-size:0.7rem;">SEND</button> 216 </div>`; 217 body.innerHTML = html; 218 219 const msgBox = $('chat-panel-messages'); 220 const filterInput = $('chat-filter-input'); 221 const nameInput = $('chat-name-input'); 222 const messageInput = $('chat-message-input'); 223 224 async function loadMsgs() { 225 try { 226 const f = filterInput.value.trim(); 227 const url = f ? `/chat?country=${encodeURIComponent(f)}` : '/chat?limit=50'; 228 const msgs = await api(url); 229 if (!msgs.length) { 230 msgBox.innerHTML = '<div class="chat-placeholder">Belum ada pesan. Mulai percakapan global!</div>'; 231 return; 232 } 233 let htmlMsgs = ''; 234 msgs.slice().reverse().forEach((m) => { 235 const t = new Date(m.created_at).toLocaleTimeString(); 236 htmlMsgs += `<div class="chat-msg"><span class="chat-msg-name">${m.member_name || 'Guest'}</span><span style="color:#64748b;"> [${m.country || ''}]</span>: ${m.message}<span class="chat-msg-time">${t}</span></div>`; 237 }); 238 msgBox.innerHTML = htmlMsgs; 239 msgBox.scrollTop = msgBox.scrollHeight; 240 } catch (err) { 241 msgBox.innerHTML = `<div class="chat-placeholder">Gagal: ${err.message}</div>`; 242 } 243 } 244 245 $('btn-apply-filter').addEventListener('click', () => { 246 window.__chatCountryFilter = filterInput.value.trim(); 247 loadMsgs(); 248 }); 249 $('btn-send-msg').addEventListener('click', async () => { 250 const name = nameInput.value.trim() || 'Guest'; 251 const msg = messageInput.value.trim(); 252 if (!msg) return; 253 const country = window.__chatCountryFilter || filterInput.value.trim() || 'Global'; 254 try { 255 await api('/chat', { 256 method: 'POST', 257 headers: { 'Content-Type': 'application/json' }, 258 body: JSON.stringify({ member_name: name, country, message: msg }) 259 }); 260 messageInput.value = ''; 261 loadMsgs(); 262 } catch (err) { 263 msgBox.innerHTML = `<div class="chat-placeholder">Gagal kirim: ${err.message}</div>`; 264 } 265 }); 266 messageInput.addEventListener('keydown', (e) => { 267 if (e.key === 'Enter') $('btn-send-msg').click(); 268 }); 269 270 loadMsgs(); 271 } 272 273 // ============ Modals ============ 274 function openJoinModal() { 275 $('join-modal').style.display = 'block'; 276 const country = window.__currentCountryName; 277 if (country) $('join-country').value = country; 278 } 279 280 function closeJoinModal() { 281 $('join-modal').style.display = 'none'; 282 } 283 284 function openEventModal() { 285 $('event-modal').style.display = 'block'; 286 const country = window.__currentCountryName; 287 if (country) $('event-country').value = country; 288 } 289 290 function closeEventModal() { 291 $('event-modal').style.display = 'none'; 292 } 293 294 async function submitJoin() { 295 const name = $('join-name').value.trim(); 296 const country = $('join-country').value.trim(); 297 const city = $('join-city').value.trim(); 298 if (!name || !country) { 299 $('join-status').textContent = 'â Nama dan Negara wajib diisi!'; 300 return; 301 } 302 const st = $('join-status'); 303 st.textContent = 'Mendaftarkan...'; 304 try { 305 // try to find coords for city via reverse lookup from loaded world data 306 let lat = null, lng = null;
307 try { 308 const wd = await fetch('/maps/world-countries.json').then(r => r.json()); 309 const feat = wd.features.find(f => f.properties.name.toLowerCase() === country.toLowerCase()); 310 if (feat && feat.geometry) { 311 const b = feat.geometry.type === 'Point' 312 ? { lat: feat.geometry.coordinates[1], lng: feat.geometry.coordinates[0] } 313 : (function () { 314 let mn = [999, 999], mx = [-999, -999]; 315 const walk = (c) => { 316 if (typeof c[0] === 'number') { mn[0] = Math.min(mn[0], c[1]); mn[1] = Math.min(mn[1], c[0]); mx[0] = Math.max(mx[0], c[1]); mx[1] = Math.max(mx[1], c[0]); } 317 else c.forEach(walk); 318 }; 319 walk(feat.geometry.coordinates); 320 return { lat: (mn[0] + mx[0]) / 2, lng: (mn[1] + mx[1]) / 2 }; 321 })(); 322 lat = b.lat; lng = b.lng; 323 } 324 } catch (e) { /* coords optional */ } 325 326 const member = await api('/members', { 327 method: 'POST', 328 headers: { 'Content-Type': 'application/json' }, 329 body: JSON.stringify({ 330 name, 331 role: $('join-role').value.trim(), 332 country, 333 city: $('join-city').value.trim(), 334 bio: $('join-bio').value.trim(), 335 lat, 336 lng 337 }) 338 }); 339 st.textContent = 'â Terdaftar! Kota Anda kini tampil di peta global.'; 340 $('join-name').value = ''; 341 $('join-role').value = ''; 342 $('join-bio').value = ''; 343 setTimeout(closeJoinModal, 1500); 344 loadMembers(); 345 renderLeaderboard(); 346 if (lat && lng && map) { 347 setTimeout(() => map.setView([lat, lng], 5), 600); 348 } 349 } catch (err) { 350 st.textContent = 'â Gagal: ' + err.message; 351 } 352 } 353 354 async function submitEvent() { 355 const title = $('event-title').value.trim(); 356 const country = $('event-country').value.trim(); 357 if (!title || !country) { 358 $('event-status').textContent = 'â Judul dan Negara wajib diisi!'; 359 return; 360 } 361 const st = $('event-status'); 362 st.textContent = 'Mempublikasikan...'; 363 try { 364 let lat = null, lng = null; 365 try { 366 const wd = await fetch('/maps/world-countries.json').then(r => r.json()); 367 const feat = wd.features.find(f => f.properties.name.toLowerCase() === country.toLowerCase()); 368 if (feat && feat.geometry) { 369 let mn = [999, 999], mx = [-999, -999]; 370 const walk = (c) => { 371 if (typeof c[0] === 'number') { mn[0] = Math.min(mn[0], c[1]); mn[1] = Math.min(mn[1], c[0]); mx[0] = Math.max(mx[0], c[1]); mx[1] = Math.max(mx[1], c[0]); } 372 else c.forEach(walk); 373 }; 374 walk(feat.geometry.coordinates); 375 lat = (mn[0] + mx[0]) / 2; lng = (mn[1] + mx[1]) / 2; 376 } 377 } catch (e) { /* optional */ } 378 379 await api('/events', { 380 method: 'POST', 381 headers: { 'Content-Type': 'application/json' }, 382 body: JSON.stringify({ 383 title, 384 country, 385 city: $('event-city').value.trim(), 386 event_type: $('event-type').value, 387 description: $('event-desc').value.trim(), 388 join_url: $('event-url').value.trim(), 389 lat, 390 lng 391 }) 392 }); 393 st.textContent = 'â Event dipublikasikan!'; 394 $('event-title').value = ''; 395 $('event-city').value = ''; 396 $('event-desc').value = ''; 397 $('event-url').value = ''; 398 setTimeout(closeEventModal, 1500); 399 loadEvents(); 400 renderEvents(); 401 } catch (err) { 402 st.textContent = 'â Gagal: ' + err.message; 403 } 404 } 405 406 // ============ Admin console ============ 407 function getAdminToken() { 408 return localStorage.getItem('__aiMapAdminToken') || ''; 409 } 410 411 function openAdminModal() { 412 $('admin-modal').style.display = 'block'; 413 $('admin-status').textContent = ''; 414 const tok = getAdminToken(); 415 if (tok) { 416 $('admin-token').value = tok; 417 $('admin-console').style.display = 'block'; 418 renderAdminLists(); 419 } else { 420 $('admin-console').style.display = 'none'; 421 } 422 } 423 424 function closeAdminModal() { 425 $('admin-modal').style.display = 'none'; 426 } 427 428 async function adminLogin() { 429 const token = $('admin-token').value.trim(); 430 const st = $('admin-status'); 431 if (!token) { st.textContent = 'â Token wajib diisi!'; return; } 432 st.textContent = 'Memverifikasi...'; 433 try { 434 const probe = await fetch(API + '/events/999999', { method: 'DELETE', headers: { 'x-admin-token': token } }); 435 if (probe.status === 401) { 436 st.textContent = 'â Token salah!'; 437 return; 438 } 439 if (probe.status === 200 || probe.status === 404) { 440 localStorage.setItem('__aiMapAdminToken', token); 441 st.textContent = 'â Terhubung ke admin console.'; 442 $('admin-console').style.display = 'block'; 443 renderAdminLists(); 444 return; 445 } 446 st.textContent = 'â Gagal (' + probe.status + ')'; 447 } catch (err) { 448 st.textContent = 'â Gagal: ' + err.message; 449 } 450 } 451 452 async function renderAdminLists() { 453 const token = getAdminToken(); 454 const ml = $('admin-member-list'); 455 const el = $('admin-event-list');
456 ml.innerHTML = '<div class="panel-empty">Memuat...</div>'; 457 el.innerHTML = '<div class="panel-empty">Memuat...</div>'; 458 try { 459 const members = await api('/members?limit=200'); 460 ml.innerHTML = members.length 461 ? members.map((m) => ` 462 <div class="admin-row"> 463 <div class="admin-row-name">${m.name}<div class="admin-row-meta">${m.role || 'Traveller'} · ${m.city || ''} ${m.country || ''}</div></div> 464 <div class="admin-row-actions"> 465 <button class="btn-admin-sm ${m.is_verified ? '' : 'accent'}" data-admin-verify="${m.id}" data-now="${m.is_verified}">${m.is_verified ? 'UNVERIFY' : 'VERIFY'}</button> 466 <button class="btn-admin-sm danger" data-admin-del="${m.id}">DEL</button> 467 </div> 468 </div>`).join('') 469 : '<div class="panel-empty">Belum ada member.</div>'; 470 471 const events = await api('/events'); 472 el.innerHTML = events.length 473 ? events.slice(0, 30).map((ev) => ` 474 <div class="admin-row"> 475 <div class="admin-row-name">${ev.title}<div class="admin-row-meta">${ev.event_type || 'Meetup'} · ${ev.country || ''}</div></div> 476 <div class="admin-row-actions"> 477 <button class="btn-admin-sm danger" data-admin-del-ev="${ev.id}">DEL</button> 478 </div> 479 </div>`).join('') 480 : '<div class="panel-empty">Belum ada event.</div>'; 481 482 ml.querySelectorAll('[data-admin-verify]').forEach((b) => { 483 b.addEventListener('click', async () => { 484 const id = b.getAttribute('data-admin-verify'); 485 const now = b.getAttribute('data-now') === 'true'; 486 try { 487 await api('/members/' + id, { 488 method: 'PATCH', 489 headers: { 'Content-Type': 'application/json', 'x-admin-token': getAdminToken() }, 490 body: JSON.stringify({ is_verified: !now }) 491 }); 492 renderAdminLists(); 493 loadMembers(); 494 renderLeaderboard(); 495 } catch (err) { $('admin-status').textContent = 'â ' + err.message; } 496 }); 497 }); 498 ml.querySelectorAll('[data-admin-del]').forEach((b) => { 499 b.addEventListener('click', async () => { 500 const id = b.getAttribute('data-admin-del'); 501 if (!confirm('Hapus member ini?')) return; 502 try { 503 await api('/members/' + id, { method: 'DELETE', headers: { 'x-admin-token': getAdminToken() } }); 504 renderAdminLists(); 505 loadMembers(); 506 renderLeaderboard(); 507 } catch (err) { $('admin-status').textContent = 'â ' + err.message; } 508 }); 509 }); 510 el.querySelectorAll('[data-admin-del-ev]').forEach((b) => { 511 b.addEventListener('click', async () => { 512 const id = b.getAttribute('data-admin-del-ev'); 513 if (!confirm('Hapus event ini?')) return; 514 try { 515 await api('/events/' + id, { method: 'DELETE', headers: { 'x-admin-token': getAdminToken() } }); 516 renderAdminLists(); 517 renderEvents(); 518 loadEvents(); 519 } catch (err) { $('admin-status').textContent = 'â ' + err.message; } 520 }); 521 }); 522 } catch (err) { 523 ml.innerHTML = `<div class="panel-empty">Gagal: ${err.message}</div>`; 524 el.innerHTML = `<div class="panel-empty">Gagal: ${err.message}</div>`; 525 } 526 } 527 528 async function deleteEvent(id, btn) { 529 const token = getAdminToken(); 530 if (!token) { 531 alert('Fitur ini hanya untuk admin. Masuk ke Admin Console dulu.'); 532 openAdminModal(); 533 return; 534 } 535 if (!confirm('Hapus event ini?')) return; 536 try { 537 await api('/events/' + id, { method: 'DELETE', headers: { 'x-admin-token': token } }); 538 if (btn) btn.closest('.event-item').remove(); 539 loadEvents(); 540 renderEvents(); 541 } catch (err) { 542 alert('Gagal hapus: ' + err.message); 543 } 544 } 545 546 // ============ UI binding ============ 547 function bindUI() { 548 // Nav links open panels 549 document.querySelectorAll('.nav-link[data-panel]').forEach((link) => { 550 link.addEventListener('click', (e) => { 551 e.preventDefault(); 552 const panel = link.getAttribute('data-panel'); 553 if (panel === 'events') renderEvents(); 554 else if (panel === 'leaderboard') renderLeaderboard(); 555 else if (panel === 'chat') renderChat(); 556 else if (panel === 'members') renderMembers(); 557 }); 558 }); 559 560 // Join button 561 $('btn-open-join').addEventListener('click', openJoinModal); 562 $('join-close-btn').addEventListener('click', closeJoinModal); 563 $('btn-submit-join').addEventListener('click', submitJoin); 564 $('join-modal').addEventListener('click', (e) => { 565 if (e.target === $('join-modal')) closeJoinModal(); 566 }); 567 568 // Event modal 569 $('event-close-btn').addEventListener('click', closeEventModal); 570 $('btn-submit-event').addEventListener('click', submitEvent); 571 $('event-modal').addEventListener('click', (e) => { 572 if (e.target === $('event-modal')) closeEventModal(); 573 }); 574 575 // Panel close 576 $('community-panel-close').addEventListener('click', closePanel); 577 578 // Admin console 579 $('btn-open-admin').addEventListener('click', openAdminModal); 580 $('admin-close-btn').addEventListener('click', closeAdminModal); 581 $('btn-admin-login').addEventListener('click', adminLogin); 582 $('admin-modal').addEventListener('click', (e) => { 583 if (e.target === $('admin-modal')) closeAdminModal(); 584 }); 585 $('admin-token').addEventListener('keydown', (e) => { 586 if (e.key === 'Enter') adminLogin(); 587 }); 588 } 589 590 // Track current country for prefills 591 const origSetCurrent = window.__setCurrentCountryName; 592 window.__setCurrentCountryName = function (name) { 593 window.__currentCountryName = name; 594 }; 595 function loadAll() { 596 loadMembers(); 597 loadEvents(); 598 } 599 600 // Expose for app.js integration 601 window.__aiCommunity = { 602 openJoinModal, 603 renderEvents, 604 renderLeaderboard, 605 renderChat, 606 renderMembers, 607 loadMembers, 608 loadEvents, 609 setCurrentCountry(name) { window.__currentCountryName = name; }, 610 openAdminModal, 611 deleteEvent, 612 getAdminToken 613 }; 614})();
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.