PageSourceSearch

https://mytriv.com/hotels/community.js?v=1786710005

js mytriv.com collected 2026-09-24 13:59:36 UTC 25,117 bytes, 614 lines download raw bytes

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 &rarr;</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 &rarr;</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.