Spaces:
Running
Running
| // GLOBAL VARIABLES | |
| let typeChart = null; | |
| let urgencyChart = null; | |
| let lastClickedId = null; | |
| let knownPostIds = new Set(); | |
| let readPostIds = new Set(); | |
| let isLoggedIn = false; // track login state | |
| let isInitialLoadComplete = false; // flag to prevent false alerts on page load | |
| let currentUsername = ''; // stores the logged-in user's username | |
| document.addEventListener("DOMContentLoaded", function() { | |
| console.log("ALISTO Dashboard Loaded"); | |
| // 1. Check Login Status Immediately | |
| checkLoginStatus(); | |
| // 2. Init Data | |
| initCharts(); | |
| fetchPosts(); | |
| fetchStats(); | |
| setInterval(() => { fetchPosts(); fetchStats(); }, 30000); | |
| // 3. Filter Listeners | |
| document.getElementById('sort-select').addEventListener('change', () => fetchPosts()); | |
| document.getElementById('view-select').addEventListener('change', () => fetchPosts()); | |
| document.getElementById('urgency-select').addEventListener('change', () => fetchPosts()); | |
| document.getElementById('type-select').addEventListener('change', () => fetchPosts()); | |
| document.getElementById('assist-select').addEventListener('change', () => fetchPosts()); | |
| // 4. Export (Check login first) | |
| document.getElementById('export-btn').addEventListener('click', () => { | |
| if(!isLoggedIn) { alert("Responders Only. Please Log In."); return; } | |
| window.location.href = '/api/export'; | |
| }); | |
| // 5. Stats Modal | |
| const statsModal = document.getElementById('stats-modal'); | |
| document.getElementById('show-stats-btn').addEventListener('click', () => { | |
| statsModal.classList.remove('hidden'); | |
| fetchStats(); | |
| }); | |
| document.getElementById('close-stats-btn').addEventListener('click', () => statsModal.classList.add('hidden')); | |
| // 6. Login Modal Logic | |
| setupLoginLogic(); | |
| setupSearch(); | |
| setupProfileDropdown(); | |
| // 🚨 The manual button listeners were correctly removed from here in the previous step. | |
| }); | |
| // ---------------------------------------------------------------------- | |
| // ACTION BUTTON VISUAL SYNC LOGIC | |
| // ---------------------------------------------------------------------- | |
| function updateActionButtons(postStatus) { | |
| const verifyBtn = document.getElementById('verify-btn'); | |
| const resolveBtn = document.getElementById('resolve-btn'); | |
| if (!verifyBtn || !resolveBtn) return; // Safety check | |
| // 1. Reset all active states first (CRITICAL STEP) | |
| verifyBtn.classList.remove('is-verified'); | |
| resolveBtn.classList.remove('is-resolved'); | |
| // Reset button text | |
| document.querySelector('#verify-btn .btn-text').textContent = 'Verify'; | |
| document.querySelector('#resolve-btn .btn-text').textContent = 'Resolve'; | |
| // 2. Apply Active State Classes based *only* on the current status | |
| if (postStatus === 'Verified') { | |
| // If Verified, apply the 'is-verified' style | |
| verifyBtn.classList.add('is-verified'); | |
| document.querySelector('#verify-btn .btn-text').textContent = 'Verified'; | |
| } else if (postStatus === 'Resolved') { | |
| // If Resolved, apply the 'is-resolved' style | |
| resolveBtn.classList.add('is-resolved'); | |
| document.querySelector('#resolve-btn .btn-text').textContent = 'Resolved'; | |
| } | |
| } | |
| // ---------------------------------------------------------------------- | |
| // AUTHENTICATION LOGIC | |
| // ---------------------------------------------------------------------- | |
| // checks the user's current login status via API | |
| function checkLoginStatus() { | |
| fetch('/api/user_status') | |
| .then(res => res.json()) | |
| .then(data => { | |
| isLoggedIn = data.is_logged_in; | |
| currentUsername = data.username || ''; | |
| updateUIForAuth(); | |
| }); | |
| } | |
| // toggles the visibility of login links, profile dropdown, and action buttons | |
| function updateUIForAuth() { | |
| const navBtn = document.getElementById('nav-login-btn'); | |
| const profileWrap = document.getElementById('profile-container-wrap'); | |
| const dropdownUsername = document.getElementById('dropdown-username'); | |
| const actionButtonsContainer = document.querySelector('.action-buttons'); | |
| if (isLoggedIn) { | |
| // LOGGED IN: Show Profile Icon, Update Name, Hide Login Link | |
| if (navBtn) navBtn.style.display = 'none'; | |
| if (profileWrap) profileWrap.style.display = 'flex'; // Show the profile icon container | |
| if (dropdownUsername) dropdownUsername.innerText = currentUsername; | |
| if (actionButtonsContainer) actionButtonsContainer.style.visibility = 'visible'; | |
| } else { | |
| // LOGGED OUT: Show Login Link, Hide Profile Icon | |
| if (navBtn) navBtn.style.display = 'inline-block'; | |
| if (profileWrap) profileWrap.style.display = 'none'; // Hide the profile icon container | |
| if (actionButtonsContainer) actionButtonsContainer.style.visibility = 'hidden'; | |
| } | |
| } | |
| // handles click events for the new profile icon and logout button inside the dropdown | |
| function setupProfileDropdown() { | |
| const profileToggle = document.getElementById('profile-toggle'); | |
| const profileDropdown = document.getElementById('profile-dropdown'); | |
| const logoutBtn = document.getElementById('dropdown-logout-btn'); | |
| // Get the reference to the existing logout modal element | |
| const logoutModal = document.getElementById('logout-modal'); | |
| // 1. Toggle visibility when clicking the icon (Unchanged) | |
| if (profileToggle) { | |
| profileToggle.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| profileDropdown.classList.toggle('hidden'); | |
| }); | |
| } | |
| // 2. Logout button handler (MODIFIED BLOCK) | |
| if (logoutBtn) { | |
| logoutBtn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| // Action: Show the confirmation modal instead of redirecting | |
| if (logoutModal) { | |
| logoutModal.classList.remove('hidden'); | |
| } else { | |
| // Fallback, should not happen if index.html is correct | |
| window.location.href = '/api/logout'; | |
| } | |
| }); | |
| } | |
| // 3. Close when clicking outside (Unchanged) | |
| document.addEventListener('click', (e) => { | |
| if (profileToggle && profileDropdown && !profileToggle.contains(e.target) && !profileDropdown.contains(e.target)) { | |
| if (!profileDropdown.classList.contains('hidden')) { | |
| profileDropdown.classList.add('hidden'); | |
| } | |
| } | |
| }); | |
| } | |
| // sets up handlers for the login and logout modals | |
| function setupLoginLogic() { | |
| const loginModal = document.getElementById('login-modal'); | |
| const logoutModal = document.getElementById('logout-modal'); | |
| const navBtn = document.getElementById('nav-login-btn'); | |
| const closeBtn = document.getElementById('close-login-btn'); | |
| const submitBtn = document.getElementById('login-submit-btn'); | |
| // 1. NAV BUTTON CLICK HANDLER | |
| navBtn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| if (isLoggedIn) { | |
| logoutModal.classList.remove('hidden'); | |
| } else { | |
| loginModal.classList.remove('hidden'); | |
| } | |
| }); | |
| // 2. LOGOUT MODAL HANDLERS | |
| document.getElementById('confirm-logout-btn').addEventListener('click', () => { | |
| window.location.href = '/api/logout'; | |
| }); | |
| document.getElementById('cancel-logout-btn').addEventListener('click', () => { | |
| logoutModal.classList.add('hidden'); | |
| }); | |
| // 3. LOGIN MODAL HANDLERS | |
| closeBtn.addEventListener('click', () => loginModal.classList.add('hidden')); | |
| submitBtn.addEventListener('click', () => { | |
| const u = document.getElementById('username').value; | |
| const p = document.getElementById('password').value; | |
| fetch('/api/login', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({username: u, password: p}) | |
| }) | |
| .then(res => { | |
| if(res.ok) return res.json(); | |
| throw new Error('Invalid credentials'); | |
| }) | |
| .then(data => { | |
| isLoggedIn = true; | |
| updateUIForAuth(); | |
| loginModal.classList.add('hidden'); | |
| document.getElementById('username').value = ''; | |
| document.getElementById('password').value = ''; | |
| document.getElementById('login-error').innerText = ''; | |
| }) | |
| .catch(err => { | |
| document.getElementById('login-error').innerText = "Invalid Username or Password"; | |
| }); | |
| }); | |
| } | |
| // calculates and formats the time difference for 'X hrs ago' | |
| function formatRelativeTime(timestamp) { | |
| const now = new Date(); | |
| const posted = new Date(timestamp); | |
| const diffMs = now - posted; | |
| const diffMins = Math.floor(diffMs / 60000); | |
| if (isNaN(diffMins)) return "Unknown time"; | |
| if (diffMins < 1) return "Just now"; | |
| if (diffMins < 60) return diffMins + " mins ago"; | |
| if (diffMins < 1440) { | |
| const hours = Math.floor(diffMins / 60); | |
| return hours + (hours === 1 ? " hr ago" : " hrs ago"); | |
| } | |
| const days = Math.floor(diffMins / 1440); | |
| return days + (days === 1 ? " day ago" : " days ago"); | |
| } | |
| // formats the absolute date and time in the specified multi-line format | |
| function formatAbsoluteDateTime(timestamp) { | |
| const posted = new Date(timestamp); | |
| // 1. Set options for the full date (e.g., "December 12, 2025") | |
| const dateOptions = { year: 'numeric', month: 'long', day: 'numeric' }; | |
| const formattedDate = posted.toLocaleDateString('en-US', dateOptions); | |
| // 2. Set options for the time (e.g., "08:52 PM") | |
| const timeOptions = { hour: '2-digit', minute: '2-digit', hour12: true }; | |
| const formattedTime = posted.toLocaleTimeString('en-US', timeOptions); | |
| // Combine them as requested | |
| return `${formattedDate}\n${formattedTime}`; | |
| } | |
| // ---------------------------------------------------------------------- | |
| // RENDER LOGIC | |
| // ---------------------------------------------------------------------- | |
| // renders the list of incident posts in the sidebar feed | |
| function renderSidebar(data) { | |
| const sidebar = document.getElementById('incident-feed'); | |
| if (!sidebar) return; | |
| sidebar.innerHTML = ''; | |
| const countEl = document.getElementById('dashboard-alert-count'); | |
| if (countEl) { | |
| countEl.innerText = data.length; | |
| } | |
| if (data.length === 0) { | |
| sidebar.innerHTML = `<p style="color: #ccc; padding: 20px; text-align: center;">No alerts found.</p>`; | |
| return; | |
| } | |
| data.forEach(post => { | |
| const box = document.createElement('div'); | |
| box.className = 'alert-box'; | |
| if (post.id === lastClickedId) box.classList.add('selected'); | |
| else if (post.status === 'New' && !readPostIds.has(post.id)) box.classList.add('unread'); | |
| const relativeTimeStr = formatRelativeTime(post.timestamp); | |
| // const timeStr = new Date(post.timestamp).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); | |
| let statusColor = '#ed4801'; /*ed4801*/ | |
| if (post.status === 'Verified') statusColor = '#ffc107'; /*00C851*/ | |
| if (post.status === 'Resolved') statusColor = '#00C851'; /*33b5e5*/ | |
| let statusText = `(${post.status})`; | |
| if (post.status === 'New' && readPostIds.has(post.id)) statusText = ""; | |
| // Determine Badge Color Class | |
| let assistClass = 'assist-badge'; // Default | |
| const type = (post.assistance_type || "").toLowerCase(); | |
| if (type.includes('medical')) assistClass += ' assist-medical'; | |
| else if (type.includes('rescue')) assistClass += ' assist-rescue'; | |
| else if (type.includes('food')) assistClass += ' assist-food'; | |
| else if (type.includes('evac')) assistClass += ' assist-evac'; | |
| box.innerHTML = ` | |
| <div class="alert-icon" style="background-color: ${statusColor}"></div> | |
| <div class="box-text"> | |
| <div class="box-title"> | |
| ${post.disaster_type} • ${post.location} | |
| <span style="font-size:0.7em; opacity:0.7">${statusText}</span> | |
| </div> | |
| <div style="display: flex; align-items: center; margin-top: 4px; gap: 5px;"> | |
| <span class="${assistClass}">${post.assistance_type || "General"}</span> | |
| <div class="box-subtitle" style="margin-top:0;">${relativeTimeStr}</div> | |
| </div> | |
| </div> | |
| `; // removed ${post.full_address} • | |
| box.addEventListener('click', () => { | |
| const detailBox = document.getElementById('postInfo'); | |
| if (lastClickedId === post.id) { | |
| detailBox.classList.remove('active'); | |
| box.classList.remove('selected'); | |
| lastClickedId = null; | |
| renderSidebar(data); | |
| } else { | |
| document.querySelectorAll('.alert-box').forEach(el => el.classList.remove('selected')); | |
| box.classList.add('selected'); | |
| lastClickedId = post.id; | |
| readPostIds.add(post.id); | |
| updateDetailView(post); | |
| detailBox.classList.add('active'); | |
| // CRITICAL: Check auth again to show/hide buttons for this specific detail view | |
| updateUIForAuth(); | |
| renderSidebar(data); | |
| } | |
| }); | |
| sidebar.appendChild(box); | |
| }); | |
| } | |
| // updates the detail panel with the selected post's information | |
| function updateDetailView(post) { | |
| const setText = (id, text) => { | |
| const el = document.getElementById(id); | |
| if (el) el.innerText = text; | |
| }; | |
| // setText('detail-title', post.title); | |
| const titleEl = document.getElementById('detail-title'); | |
| if (titleEl) { | |
| // Safely extract and capitalize the disaster type | |
| const disaster = (post.disaster_type || 'INCIDENT').toUpperCase(); | |
| // Safely extract and capitalize the location (City) | |
| const locationName = (post.location || 'UNKNOWN LOCATION').toUpperCase(); | |
| // Set the new title format | |
| titleEl.textContent = `${disaster} in ${locationName}`; | |
| } | |
| setText('detail-location', post.full_address || "Unknown Address"); | |
| setText('detail-assistance', post.assistance_type || "General"); | |
| setText('detail-contact-name', post.author ? ( | |
| // Check if the name contains a space (suggests a full name) | |
| // If no space is found, assume it is a username and prepend 'u/' | |
| post.author.includes(' ') ? post.author : `u/${post.author}` | |
| ) : "Unknown"); | |
| setText('detail-contact-number', post.contact_number || "Check Post"); | |
| // setText('detail-body', post.content); | |
| const bodyEl = document.getElementById('detail-body'); | |
| if (bodyEl) { | |
| const postTitle = post.title || 'No Original Title Provided'; | |
| const postBody = post.content || 'No Body Text Provided'; | |
| // Use HTML structure to clearly present both original title and body. | |
| bodyEl.innerHTML = ` | |
| <div> | |
| <p style="font-weight: bold; margin-bottom: 5px; color: #ddd; font-size: 1.15em;">${postTitle}</p> | |
| </div> | |
| <p style="font-size: 1.05em; line-height: 1.5;">${postBody}</p> | |
| `; // <p style="font-size: 0.75em; line-height: 1.5;">Post content: </p> | |
| } | |
| setText('detail-status', post.status); | |
| const statusBadge = document.getElementById('detail-status'); | |
| if (statusBadge) { | |
| // Clear all previous status classes | |
| statusBadge.classList.remove('status-new', 'status-verified', 'status-resolved'); | |
| // Apply the correct new class | |
| if (post.status) { | |
| statusBadge.classList.add(`status-${post.status.toLowerCase()}`); | |
| } | |
| } | |
| const link = document.getElementById('detail-link'); | |
| if (link) { | |
| const isSimulated = typeof post.reddit_id === 'string' && (post.reddit_id.startsWith('fake') || post.reddit_id.startsWith('sim')); | |
| link.href = isSimulated ? '#' : `https://reddit.com/comments/${post.reddit_id.replace('t3_', '')}`; | |
| } | |
| const urgEl = document.getElementById('detail-urgency'); | |
| if (urgEl) { | |
| urgEl.innerText = post.urgency_level; | |
| urgEl.style.color = post.urgency_level === 'High' ? '#ff4444' : '#00C851'; | |
| } | |
| const timeEl = document.getElementById('detail-time'); | |
| if (timeEl) { | |
| // 1. Get the Date object from the post timestamp | |
| const posted = new Date(post.timestamp); | |
| // 2. Format the Date part (e.g., December 12, 2025) | |
| const dateStr = posted.toLocaleDateString('en-US', { | |
| year: 'numeric', | |
| month: 'long', | |
| day: 'numeric' | |
| }); | |
| // 3. Format the Time part (e.g., 08:52 PM) | |
| const exactTimeStr = posted.toLocaleTimeString([], { | |
| hour: '2-digit', | |
| minute: '2-digit', | |
| hour12: true | |
| }); | |
| // 4. Generate the new HTML structure | |
| timeEl.innerHTML = ` | |
| <div style="font-size: 1.3em; color: white; line-height: 1.2;">${dateStr}</div> | |
| <div style="font-size: 1.1em; color: #ed4801; margin-top: 2px;">${exactTimeStr}</div> | |
| `; | |
| } | |
| // Synchronize buttons with the loaded post status | |
| updateActionButtons(post.status); | |
| } | |
| // ---------------------------------------------------------------------- | |
| // STANDARD FUNCTIONS (FINAL WORKING VERSION) | |
| // ---------------------------------------------------------------------- | |
| // handles the logic for updating the post status (Verify/Resolve) | |
| function updateStatus(intendedStatus) { | |
| if (!lastClickedId) return; | |
| const badge = document.getElementById('detail-status'); | |
| // FIX: Read status and clean it by converting to lowercase and trimming whitespace | |
| const currentStatus = badge ? badge.innerText.trim() : 'New'; | |
| let finalStatus; | |
| if (intendedStatus === 'Verified') { | |
| // ... (rest of the logic) | |
| // Check against the current, clean status | |
| if (currentStatus.toLowerCase() === 'verified') { | |
| finalStatus = 'New'; | |
| } else { | |
| finalStatus = 'Verified'; | |
| } | |
| } else if (intendedStatus === 'Resolved') { | |
| // ... (rest of the logic) | |
| // Check against the current, clean status | |
| if (currentStatus.toLowerCase() === 'resolved') { | |
| finalStatus = 'New'; | |
| } else { | |
| finalStatus = 'Resolved'; | |
| } | |
| } else { | |
| return; | |
| } | |
| // Safety check: ensure we are actually changing the status | |
| if (finalStatus === currentStatus) { | |
| return; | |
| } | |
| // --- API CALL AND UI UPDATE --- | |
| fetch(`/api/posts/${lastClickedId}/status`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ status: finalStatus }) | |
| }) | |
| .then(res => { | |
| if(res.status === 401) { alert("Unauthorized. Please log in."); return; } | |
| // If the API call returns success (200), proceed with UI updates based on the finalStatus. | |
| return res.json(); | |
| }) | |
| .then(data => { | |
| if(data) { | |
| fetchPosts(); | |
| fetchStats(); | |
| if (badge) { | |
| // 1. Update text | |
| badge.innerText = finalStatus; | |
| // 2. Apply color class (visual sync fix) | |
| badge.classList.remove('status-new', 'status-verified', 'status-resolved'); | |
| badge.classList.add(`status-${finalStatus.toLowerCase()}`); | |
| } | |
| // 3. Update buttons' appearance | |
| updateActionButtons(finalStatus); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error("Status Update Failed:", error); | |
| alert("Failed to update status due to network error."); | |
| }); | |
| } | |
| // pop up notificaiton for new alerts | |
| function showNotification(message) { | |
| const popup = document.getElementById('new-alert-notification'); | |
| const msgEl = document.getElementById('notification-message'); | |
| if (!popup || !msgEl) return; | |
| msgEl.innerText = message; | |
| // 1. Prepare for animation (ensure it's visible but off-screen) | |
| popup.classList.remove('hidden'); | |
| // 2. Force reflow to ensure CSS animation starts correctly | |
| void popup.offsetWidth; | |
| // 3. Trigger slide-in animation | |
| popup.classList.add('visible'); | |
| // 4. Set timeout to slide out after 6 seconds | |
| setTimeout(() => { | |
| popup.classList.remove('visible'); | |
| // 5. Hide completely after animation finishes (0.5s transition time in CSS) | |
| setTimeout(() => { | |
| popup.classList.add('hidden'); | |
| }, 500); | |
| }, 6000); // Display for 6 seconds | |
| } | |
| // initializes and draws the chart.js graphs for stats modal | |
| function initCharts() { | |
| const ctxType = document.getElementById('typeChart'); | |
| const ctxUrg = document.getElementById('urgencyChart'); | |
| if (!ctxType || !ctxUrg) return; | |
| typeChart = new Chart(ctxType.getContext('2d'), { | |
| type: 'doughnut', | |
| data: { labels: [], datasets: [{ data: [], backgroundColor: ['#ed4801', '#33b5e5', '#00C851', '#ffbb33', '#aa66cc'], borderWidth: 0 }] }, | |
| options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: 'white' } } } } | |
| }); | |
| urgencyChart = new Chart(ctxUrg.getContext('2d'), { | |
| type: 'bar', | |
| data: { labels: ['High', 'Low/Med'], datasets: [{ label: 'Count', data: [0, 0], backgroundColor: ['#ff4444', '#00C851'], borderRadius: 5 }] }, | |
| options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.1)' }, ticks: { color: 'white' } }, x: { ticks: { color: 'white' } } }, plugins: { legend: { display: false } } } | |
| }); | |
| } | |
| // fetches statistical data from the API | |
| function fetchStats() { fetch('/api/stats').then(res=>res.json()).then(data=>updateCharts(data)).catch(err=>console.error(err)); } | |
| // updates the chart data and redraws the graphs | |
| function updateCharts(data) { if(!typeChart || !urgencyChart) return; const types = data.disaster_types || {}; typeChart.data.labels = Object.keys(types); typeChart.data.datasets[0].data = Object.values(types); typeChart.update(); const levels = data.urgency_levels || {}; urgencyChart.data.datasets[0].data = [levels['High']||0, (levels['Low']||0)+(levels['Medium']||0)]; urgencyChart.update(); } | |
| // fetches post data from the API based on current filter selections | |
| function fetchPosts(q='') { | |
| const sort = document.getElementById('sort-select')?.value||'newest'; | |
| const view = document.getElementById('view-select')?.value||'active'; | |
| const urgency = document.getElementById('urgency-select')?.value||'all'; | |
| const type = document.getElementById('type-select')?.value||'all'; | |
| const assist = document.getElementById('assist-select')?.value||'all'; | |
| const searchVal = document.querySelector('.search-input')?.value||''; | |
| let url = `/api/posts?sort=${sort}&view=${view}&urgency=${urgency}&type=${type}&assist=${assist}`; | |
| if(searchVal) url += `&query=${encodeURIComponent(searchVal)}`; | |
| url += `&_=${new Date().getTime()}`; | |
| fetch(url).then(r=>r.json()).then(d=>{renderSidebar(d); checkAudioAlert(d);}).catch(e=>console.error(e)); | |
| } | |
| // checks for new high-urgency alerts and plays sound + shows notification | |
| function checkAudioAlert(p){ | |
| const a=document.getElementById('alert-sound'); | |
| if(!a)return; | |
| a.volume = 0.1; | |
| let newAlertFound = false; | |
| if (!isInitialLoadComplete) { | |
| // 1. On the very first load after page navigation/refresh: | |
| // Populate the known set with ALL current IDs and skip the alert. | |
| p.forEach(x => knownPostIds.add(x.id)); | |
| isInitialLoadComplete = true; | |
| return; | |
| } | |
| p.forEach(x => { | |
| if(x.urgency_level === 'High' && !knownPostIds.has(x.id) && x.status !== 'Resolved') { | |
| newAlertFound = true; | |
| } | |
| }); | |
| if(newAlertFound) { | |
| a.play().catch(e=>{}); | |
| showNotification("NEW HIGH PRIORITY ALERT: Check Feed"); | |
| } | |
| p.forEach(x => { | |
| knownPostIds.add(x.id); | |
| }); | |
| } | |
| // sets up the search input logic with clear button | |
| function setupSearch(){ const i=document.querySelector('.search-input'), c=document.querySelector('.clear-icon'); if(!i)return; i.addEventListener('input',()=>{if(i.value)c?.classList.remove('hidden');else{c?.classList.add('hidden');fetchPosts();}}); i.addEventListener('keydown',e=>{if(e.key==='Enter')fetchPosts();}); c?.addEventListener('click',()=>{i.value='';c.classList.add('hidden');fetchPosts();}); } | |
| // Attach listeners once DOM is ready | |
| (function () { | |
| // Select all elements with data_tooltip attribute | |
| const tooltipElements = document.querySelectorAll('[data_tooltip]'); | |
| tooltipElements.forEach(el => { | |
| // When clicked: hide tooltip immediately by adding class | |
| el.addEventListener('mousedown', (e) => { | |
| // Add class so CSS hides tooltip; use mousedown for immediate feedback | |
| el.classList.add('tooltip-hidden'); | |
| // Also remove focus so :focus doesn't keep hiding/showing unpredictably | |
| if (typeof el.blur === 'function') { | |
| el.blur(); | |
| } | |
| }); | |
| // When mouse leaves the element: remove the hiding class so future hovers work | |
| el.addEventListener('mouseleave', (e) => { | |
| el.classList.remove('tooltip-hidden'); | |
| }); | |
| // Also remove hidden class on touchend for touch devices (optional) | |
| el.addEventListener('touchend', () => { | |
| el.classList.remove('tooltip-hidden'); | |
| }); | |
| }); | |
| })(); |