/** * FLOCK SAFETY TRANSPARENCY PORTALS DIRECTORY * Client Application Logic * High-performance search, filtering, pagination, modal inspector, and CSV export. */ (function() { 'use strict'; // --- APPLICATION STATE --- const state = { allPortals: [], orgsLookup: [], summary: {}, filteredPortals: [], currentPage: 1, pageSize: 24, currentView: 'cards', // 'cards' or 'table' searchQuery: '', selectedState: 'ALL', selectedType: 'ALL', auditOnly: false, sortBy: 'cameras-desc', activeModalPortal: null }; // --- DOM ELEMENTS --- const el = { // Search & Filters searchInput: document.getElementById('search-input'), clearSearchBtn: document.getElementById('clear-search-btn'), stateFilter: document.getElementById('state-filter'), typeFilter: document.getElementById('type-filter'), sortSelect: document.getElementById('sort-select'), auditToggle: document.getElementById('audit-toggle'), btnResetAll: document.getElementById('btn-reset-all'), stateChipsScroll: document.getElementById('state-chips-scroll'), // Stats Counters statPortals: document.getElementById('stat-portals'), statCameras: document.getElementById('stat-cameras'), statVehicles: document.getElementById('stat-vehicles'), statSearches: document.getElementById('stat-searches'), statHits: document.getElementById('stat-hits'), // Views & Results resultsCounter: document.getElementById('results-counter'), showingCount: document.getElementById('showing-count'), totalCount: document.getElementById('total-count'), portalsGrid: document.getElementById('portals-grid'), tableContainer: document.getElementById('table-container'), tableBody: document.getElementById('table-body'), paginationControls: document.getElementById('pagination-controls'), viewCardsBtn: document.getElementById('view-cards-btn'), viewTableBtn: document.getElementById('view-table-btn'), btnExportCsv: document.getElementById('btn-export-csv'), // Modal Elements agencyModal: document.getElementById('agency-modal'), modalCloseBtn: document.getElementById('modal-close-btn'), modalAgencyName: document.getElementById('modal-agency-name'), modalAgencyLocation: document.getElementById('modal-agency-location'), modalAgencySlug: document.getElementById('modal-agency-slug'), modalPortalUrl: document.getElementById('modal-portal-url'), modalStatCameras: document.getElementById('modal-stat-cameras'), modalStatVehicles: document.getElementById('modal-stat-vehicles'), modalStatSearches: document.getElementById('modal-stat-searches'), modalStatHits: document.getElementById('modal-stat-hits'), modalReasonsSection: document.getElementById('modal-reasons-section'), modalReasonsList: document.getElementById('modal-reasons-list'), modalSharingCount: document.getElementById('modal-sharing-count'), modalSharingSearch: document.getElementById('modal-sharing-search'), modalSharingBox: document.getElementById('modal-sharing-box'), modalProhibitedText: document.getElementById('modal-prohibited-text'), modalGoogleContractLink: document.getElementById('modal-google-contract-link'), modalCopyLinkBtn: document.getElementById('modal-copy-link-btn'), // Directory Specific Camera Nodes Elements directoryCamerasSection: document.getElementById('directory-cameras-section'), dirCamQueryTitle: document.getElementById('dir-cam-query-title'), dirCamSubtitle: document.getElementById('dir-cam-subtitle'), dirCamBadgeCount: document.getElementById('dir-cam-badge-count'), directoryCamerasGrid: document.getElementById('directory-cameras-grid'), // Toast Container toastContainer: document.getElementById('toast-container') }; // --- INITIALIZATION --- function init() { if (!window.FLOCK_DATA || !window.FLOCK_DATA.portals) { showToast('Error: Dataset failed to load.', 'error'); console.error('FLOCK_DATA not found'); return; } state.allPortals = window.FLOCK_DATA.portals || []; state.orgsLookup = window.FLOCK_DATA.orgs || []; state.summary = window.FLOCK_DATA.summary || {}; // Register Local Sheriff Corridors so Directory search & filters match 100% const localSheriffPortals = [ { name: "Palmdale Station - Los Angeles County Sheriff (LASD)", location: "Palmdale, CA", state: "CA", dept_type: "SO", slug: "palmdale-ca-lasd", cameras: 28, vehicles_captured: 1850000, searches: 14200, hotlist_hits: 820, has_search_audit: true, url: "https://transparency.flocksafety.com/palmdale-ca-lasd", shared_org_ids: [15, 73, 102, 145, 180, 204], top_search_reasons: { "10851 CVC Stolen Vehicle": 4120, "Felony Warrant / BOLO": 2180, "Amber Alert / Missing Person": 410, "Robbery / Assault Investigation": 1890 }, prohibited_uses: "Prohibited from: Traffic speed ticketing, red light ticketing, facial recognition, or immigration enforcement." }, { name: "Lancaster Station - Los Angeles County Sheriff (LASD)", location: "Lancaster, CA", state: "CA", dept_type: "SO", slug: "lancaster-ca-lasd", cameras: 32, vehicles_captured: 2100000, searches: 16800, hotlist_hits: 950, has_search_audit: true, url: "https://transparency.flocksafety.com/lancaster-ca-lasd", shared_org_ids: [15, 73, 102, 145, 180, 204], top_search_reasons: { "10851 CVC Stolen Vehicle": 4850, "Felony Warrant / BOLO": 2400, "Amber Alert / Missing Person": 520, "Commercial Burglary Investigation": 2100 }, prohibited_uses: "Prohibited from: Traffic speed ticketing, red light ticketing, facial recognition, or immigration enforcement." } ]; localSheriffPortals.forEach(lp => { if (!state.allPortals.some(p => p.slug === lp.slug)) { state.allPortals.unshift(lp); } }); // Restore saved view preference const savedView = localStorage.getItem('flock_dir_view'); if (savedView === 'table' || savedView === 'cards') { state.currentView = savedView; } renderHeroStats(); populateStateFilter(); renderStateChips(); bindEvents(); // Initial Filter & Render applyFilters(); updateViewMode(); // Check URL Hash for deep link checkUrlHash(); } // --- HERO STATS FORMATTING --- function renderHeroStats() { const s = state.summary; if (!s) return; if (s.total_portals_found) { el.statPortals.textContent = s.total_portals_found.toLocaleString(); } if (s.total_cameras) { el.statCameras.textContent = s.total_cameras.toLocaleString() + '+'; } if (s.total_vehicles_captured) { const millions = (s.total_vehicles_captured / 1000000).toFixed(1); el.statVehicles.textContent = millions + 'M'; } if (s.total_searches) { el.statSearches.textContent = s.total_searches.toLocaleString(); } if (s.total_hotlist_hits) { const millions = (s.total_hotlist_hits / 1000000).toFixed(1); el.statHits.textContent = millions + 'M+'; } } // --- POPULATE STATE SELECTOR & CHIPS --- function populateStateFilter() { const stateCounts = {}; state.allPortals.forEach(p => { const st = p.state || 'OTHER'; stateCounts[st] = (stateCounts[st] || 0) + 1; }); const sortedStates = Object.keys(stateCounts).sort(); // Clear existing options except ALL el.stateFilter.innerHTML = ``; sortedStates.forEach(st => { const opt = document.createElement('option'); opt.value = st; opt.textContent = `${getStateFullName(st)} (${st}) β€” ${stateCounts[st]} portals`; el.stateFilter.appendChild(opt); }); } function renderStateChips() { const stateCounts = {}; state.allPortals.forEach(p => { const st = p.state || 'OTHER'; stateCounts[st] = (stateCounts[st] || 0) + 1; }); // Top states by portal count const topStates = Object.entries(stateCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 16); let chipsHtml = ` `; topStates.forEach(([st, count]) => { const isActive = state.selectedState === st; chipsHtml += ` `; }); el.stateChipsScroll.innerHTML = chipsHtml; } // --- STATE FULL NAME HELPER --- const stateNames = { AL: 'Alabama', AK: 'Alaska', AZ: 'Arizona', AR: 'Arkansas', CA: 'California', CO: 'Colorado', CT: 'Connecticut', DE: 'Delaware', FL: 'Florida', GA: 'Georgia', HI: 'Hawaii', ID: 'Idaho', IL: 'Illinois', IN: 'Indiana', IA: 'Iowa', KS: 'Kansas', KY: 'Kentucky', LA: 'Louisiana', ME: 'Maine', MD: 'Maryland', MA: 'Massachusetts', MI: 'Michigan', MN: 'Minnesota', MS: 'Mississippi', MO: 'Missouri', MT: 'Montana', NE: 'Nebraska', NV: 'Nevada', NH: 'New Hampshire', NJ: 'New Jersey', NM: 'New Mexico', NY: 'New York', NC: 'North Carolina', ND: 'North Dakota', OH: 'Ohio', OK: 'Oklahoma', OR: 'Oregon', PA: 'Pennsylvania', RI: 'Rhode Island', SC: 'South Carolina', SD: 'South Dakota', TN: 'Tennessee', TX: 'Texas', UT: 'Utah', VT: 'Vermont', VA: 'Virginia', WA: 'Washington', WV: 'West Virginia', WI: 'Wisconsin', WY: 'Wyoming' }; function getStateFullName(abbr) { return stateNames[abbr] || abbr; } // --- FILTER & SORT LOGIC --- function applyFilters() { const q = state.searchQuery.trim().toLowerCase(); const st = state.selectedState; const deptType = state.selectedType; const auditOnly = state.auditOnly; let filtered = state.allPortals.filter(p => { // Search query filter if (q) { const matchName = (p.name || '').toLowerCase().includes(q); const matchLoc = (p.location || '').toLowerCase().includes(q); const matchSlug = (p.slug || '').toLowerCase().includes(q); const matchState = (p.state || '').toLowerCase().includes(q); const matchStateFull = getStateFullName(p.state).toLowerCase().includes(q); if (!matchName && !matchLoc && !matchSlug && !matchState && !matchStateFull) { return false; } } // State filter if (st !== 'ALL' && p.state !== st) { return false; } // Department type filter if (deptType !== 'ALL') { const pType = (p.dept_type || '').toUpperCase(); if (deptType === 'PD' && pType !== 'PD') return false; if (deptType === 'SO' && pType !== 'SO') return false; if (deptType === 'OTHER' && (pType === 'PD' || pType === 'SO')) return false; } // Audit status filter if (auditOnly && !p.has_search_audit) { return false; } return true; }); // Sorting filtered.sort((a, b) => { switch (state.sortBy) { case 'cameras-desc': return (b.cameras || 0) - (a.cameras || 0); case 'vehicles-desc': return (b.vehicles_captured || 0) - (a.vehicles_captured || 0); case 'searches-desc': return (b.searches || 0) - (a.searches || 0); case 'hits-desc': return (b.hotlist_hits || 0) - (a.hotlist_hits || 0); case 'alpha-asc': return (a.name || '').localeCompare(b.name || ''); case 'alpha-desc': return (b.name || '').localeCompare(a.name || ''); default: return 0; } }); state.filteredPortals = filtered; state.currentPage = 1; renderDirectory(); } // --- RENDER SPECIFIC CAMERA NODES & HARDWARE IMAGES IN DIRECTORY --- function renderDirectoryCameras(rawQuery, filteredPortals) { if (!el.directoryCamerasSection || !el.directoryCamerasGrid) return; const q = (rawQuery || '').trim().toLowerCase(); const allCams = (typeof window.getFlockAllCameras === 'function') ? window.getFlockAllCameras() : []; if (allCams.length === 0) { el.directoryCamerasSection.style.display = 'none'; return; } let matchingCams = []; if (q) { matchingCams = allCams.filter(c => { const nameMatch = (c.name || '').toLowerCase().includes(q); const agencyMatch = (c.agency || '').toLowerCase().includes(q); const slugMatch = (c.slug || '').toLowerCase().includes(q); const notesMatch = (c.notes || '').toLowerCase().includes(q); const cityMatch = (c.city || '').toLowerCase().includes(q); const crossMatch = (c.crossStreets || '').toLowerCase().includes(q); const stateMatch = (c.state || '').toLowerCase() === q; return nameMatch || agencyMatch || slugMatch || notesMatch || cityMatch || crossMatch || stateMatch; }); // Filter by state if state dropdown is not ALL if (state.selectedState && state.selectedState !== 'ALL') { matchingCams = matchingCams.filter(c => { if (c.state) return c.state.toUpperCase() === state.selectedState.toUpperCase(); if (state.selectedState === 'CA') return c.id.includes('plm') || c.id.includes('lan') || c.id.includes('cal'); return true; }); } // Filter by agency type if not ALL if (state.selectedType && state.selectedType !== 'ALL') { matchingCams = matchingCams.filter(c => { if (c.dept_type) return c.dept_type.toUpperCase() === state.selectedType.toUpperCase(); if (state.selectedType === 'SO') return (c.agency && (c.agency.toLowerCase().includes('sheriff') || c.agency.includes('LASD'))); if (state.selectedType === 'PD') return (c.agency && (c.agency.toLowerCase().includes('police') || c.agency.includes('PD'))); return true; }); } } else if (state.selectedState && state.selectedState !== 'ALL') { matchingCams = allCams.filter(c => { let stMatch = false; if (c.state) stMatch = (c.state.toUpperCase() === state.selectedState.toUpperCase()); else if (state.selectedState === 'CA') stMatch = c.id.includes('plm') || c.id.includes('lan') || c.id.includes('cal'); else if (state.selectedState === 'KY') stMatch = c.id.includes('lex'); else if (state.selectedState === 'GA') stMatch = c.id.includes('atl'); else if (state.selectedState === 'TX') stMatch = c.id.includes('dfw'); if (!stMatch) return false; if (state.selectedType && state.selectedType !== 'ALL') { if (c.dept_type) return c.dept_type.toUpperCase() === state.selectedType.toUpperCase(); if (state.selectedType === 'SO') return (c.agency && (c.agency.toLowerCase().includes('sheriff') || c.agency.includes('LASD'))); if (state.selectedType === 'PD') return (c.agency && (c.agency.toLowerCase().includes('police') || c.agency.includes('PD'))); } return true; }); } if (matchingCams.length === 0) { el.directoryCamerasSection.style.display = 'none'; return; } // Show section el.directoryCamerasSection.style.display = 'block'; // Set Header Title let titleText = 'All Mapped Surveillance Corridors'; if (q) { titleText = `"${escapeHtml(rawQuery)}"`; } else if (state.selectedState !== 'ALL') { titleText = `${state.selectedState} Corridor Locations`; } el.dirCamQueryTitle.innerHTML = titleText; el.dirCamBadgeCount.textContent = `${matchingCams.length} Camera Nodes Mapped`; // Render Camera Cards with Images, Cross-Streets, and Live Actions const cardsHtml = matchingCams.map(cam => { const meta = (window.FlockAuth && window.FlockAuth.getCameraMeta) ? window.FlockAuth.getCameraMeta(cam.id) : { confirmations: 1, hasPhoto: false, isContested: false }; const photoUrl = meta.photoUrl || cam.photoUrl || null; const hasPhoto = !!photoUrl; const crossStreets = cam.crossStreets || (cam.name ? cam.name.split('(')[0].replace(/^.*?-\s*/, '').trim() : 'Intersection Camera'); const badgeHtml = meta.isContested ? `🟠 CONTESTED` : (hasPhoto ? `🟒 HARDWARE PHOTO VERIFIED` : `⚠️ SIGHTING RECORDED`); const imageContainerHtml = hasPhoto ? `
Camera at ${escapeHtml(crossStreets)} ${badgeHtml}
πŸ” Tap to inspect photo proof
` : `
πŸ“Έ Hardware Photo Pending Mounted on utility / signal pole
${badgeHtml}
`; return `
${imageContainerHtml}
πŸ“
CROSS STREETS ${escapeHtml(crossStreets)}

${escapeHtml(cam.name)}

πŸ›οΈ ${escapeHtml(cam.agency || 'Local Law Enforcement')} ${cam.city ? `πŸ—ΊοΈ ${escapeHtml(cam.city)}, ${escapeHtml(cam.state || 'CA')}` : ''}
HARDWARE ${escapeHtml(cam.type || 'Falcon Solar ALPR')}
MONTHLY SCANS ${escapeHtml(cam.captures30d ? `${cam.captures30d}` : '300,000+')}
COORDINATES ${cam.lat.toFixed(4)}, ${cam.lng.toFixed(4)}
CONSENSUS πŸ‘ ${meta.confirmations || 1} Confirmed
`; }).join(''); el.directoryCamerasGrid.innerHTML = cardsHtml; } // --- RENDER MAIN DIRECTORY --- function renderDirectory() { // Render specific camera nodes with photos and cross streets if matching renderDirectoryCameras(state.searchQuery, state.filteredPortals); const total = state.filteredPortals.length; el.totalCount.textContent = state.allPortals.length.toLocaleString(); el.showingCount.textContent = total.toLocaleString(); // Pagination slice const startIndex = (state.currentPage - 1) * state.pageSize; const endIndex = Math.min(startIndex + state.pageSize, total); const pagePortals = state.filteredPortals.slice(startIndex, endIndex); if (total === 0) { const emptyHtml = `

No Matching Portals Found

No Flock transparency portals matched your criteria: "${state.searchQuery || state.selectedState}".

`; el.portalsGrid.innerHTML = emptyHtml; el.tableBody.innerHTML = `No portals found matching search criteria.`; el.paginationControls.innerHTML = ''; document.getElementById('btn-empty-reset')?.addEventListener('click', resetAllFilters); return; } if (state.currentView === 'cards') { renderCards(pagePortals); } else { renderTable(pagePortals); } renderPagination(total); } // --- RENDER CARDS VIEW --- function renderCards(portals) { const cardsHtml = portals.map(p => { const camerasStr = p.cameras ? p.cameras.toLocaleString() : 'β€”'; const vehiclesStr = p.vehicles_captured ? formatNumberCompact(p.vehicles_captured) : 'β€”'; const searchesStr = p.searches ? p.searches.toLocaleString() : 'β€”'; const hitsStr = p.hotlist_hits ? p.hotlist_hits.toLocaleString() : 'β€”'; const sharedCount = p.shared_count || 0; return `
${escapeHtml(p.state)} ${escapeHtml(p.dept_type || 'PD')}

${escapeHtml(p.name)}

${escapeHtml(p.location || p.name)}, ${escapeHtml(p.state)}
${camerasStr} Cameras
${vehiclesStr} 30d Captures
${searchesStr} Searches
${hitsStr} Hotlist Hits
${p.has_search_audit ? ` Search Audit Public ` : ''} ${sharedCount > 0 ? ` ` : ''}
`; }).join(''); el.portalsGrid.innerHTML = cardsHtml; } // --- RENDER TABLE VIEW --- function renderTable(portals) { const tableHtml = portals.map(p => { const camerasStr = p.cameras ? p.cameras.toLocaleString() : 'β€”'; const vehiclesStr = p.vehicles_captured ? p.vehicles_captured.toLocaleString() : 'β€”'; const searchesStr = p.searches ? p.searches.toLocaleString() : 'β€”'; const hitsStr = p.hotlist_hits ? p.hotlist_hits.toLocaleString() : 'β€”'; const sharedCount = p.shared_count || 0; return `
${escapeHtml(p.name)} ${escapeHtml(p.slug)}
${escapeHtml(p.state)} ${camerasStr} ${vehiclesStr} ${searchesStr} ${hitsStr} ${sharedCount} orgs
β†—
`; }).join(''); el.tableBody.innerHTML = tableHtml; } // --- PAGINATION RENDERER --- function renderPagination(total) { const totalPages = Math.ceil(total / state.pageSize); if (totalPages <= 1) { el.paginationControls.innerHTML = ''; return; } let pagesHtml = ''; // Previous Button pagesHtml += ` `; // Windowed Page Numbers const current = state.currentPage; let startPage = Math.max(1, current - 2); let endPage = Math.min(totalPages, current + 2); if (startPage > 1) { pagesHtml += ``; if (startPage > 2) pagesHtml += `...`; } for (let i = startPage; i <= endPage; i++) { pagesHtml += ` `; } if (endPage < totalPages) { if (endPage < totalPages - 1) pagesHtml += `...`; pagesHtml += ``; } // Next Button pagesHtml += ` `; el.paginationControls.innerHTML = pagesHtml; } // --- DEEP AGENCY MODAL HANDLER --- function openAgencyModal(slug) { const portal = state.allPortals.find(p => p.slug === slug); if (!portal) { showToast('Agency record not found.', 'error'); return; } state.activeModalPortal = portal; // Fill Basic Details el.modalAgencyName.textContent = portal.name; el.modalAgencyLocation.textContent = `${portal.location || portal.name}, ${portal.state}`; el.modalAgencySlug.textContent = `transparency.flocksafety.com/${portal.slug}`; el.modalPortalUrl.href = portal.url; // Stats el.modalStatCameras.textContent = portal.cameras ? portal.cameras.toLocaleString() : 'Not Reported'; el.modalStatVehicles.textContent = portal.vehicles_captured ? portal.vehicles_captured.toLocaleString() : 'Not Reported'; el.modalStatSearches.textContent = portal.searches ? portal.searches.toLocaleString() : '0'; el.modalStatHits.textContent = portal.hotlist_hits ? portal.hotlist_hits.toLocaleString() : '0'; // Search Justifications / Reasons const reasons = portal.top_search_reasons || {}; const reasonEntries = Object.entries(reasons).sort((a, b) => b[1] - a[1]); if (reasonEntries.length > 0) { el.modalReasonsSection.style.display = 'block'; el.modalReasonsList.innerHTML = reasonEntries.map(([reason, count]) => `
${escapeHtml(reason)} ${count.toLocaleString()} searches
`).join(''); } else { el.modalReasonsSection.style.display = 'none'; } // Inter-Agency Sharing Network const sharedIds = portal.shared_org_ids || []; el.modalSharingCount.textContent = sharedIds.length.toLocaleString(); renderModalSharingTags(sharedIds, ''); // Reset sharing search input el.modalSharingSearch.value = ''; // Prohibited Uses Text el.modalProhibitedText.textContent = portal.prohibited_uses || 'Prohibited uses standardly include: Immigration enforcement, general traffic speed citations, harassment, or personal/non-official inquiries.'; // Research Tool Links const googleQuery = encodeURIComponent(`"${portal.name}" "Flock Safety" contract OR council OR resolution`); el.modalGoogleContractLink.href = `https://www.google.com/search?q=${googleQuery}`; // Set URL Hash window.location.hash = portal.slug; // Open Native Dialog if (typeof el.agencyModal.showModal === 'function') { el.agencyModal.showModal(); } else { el.agencyModal.setAttribute('open', ''); } } // Global access so radar map can open agency modal without switching views window.openAgencyModal = openAgencyModal; function renderModalSharingTags(sharedIds, query) { if (!sharedIds || sharedIds.length === 0) { el.modalSharingBox.innerHTML = `No external sharing partners published on portal.`; return; } const q = (query || '').toLowerCase().trim(); let orgNames = sharedIds.map(id => state.orgsLookup[id] || 'Unknown Agency'); if (q) { orgNames = orgNames.filter(name => name.toLowerCase().includes(q)); } if (orgNames.length === 0) { el.modalSharingBox.innerHTML = `No matching partner agencies.`; return; } // Show up to 200 partner agencies in the box const displayOrgs = orgNames.slice(0, 250); const tagsHtml = displayOrgs.map(name => ` ${escapeHtml(name)} `).join(''); const moreNotice = orgNames.length > 250 ? `...and ${(orgNames.length - 250).toLocaleString()} more. Use search to filter.` : ''; el.modalSharingBox.innerHTML = tagsHtml + moreNotice; } function closeAgencyModal() { if (typeof el.agencyModal.close === 'function') { el.agencyModal.close(); } else { el.agencyModal.removeAttribute('open'); } state.activeModalPortal = null; // Clear hash without reloading history.replaceState(null, document.title, window.location.pathname + window.location.search); } // --- CSV EXPORT FUNCTIONALITY --- function exportFilteredToCsv() { const list = state.filteredPortals; if (!list || list.length === 0) { showToast('No portals to export.', 'warning'); return; } const headers = [ 'Agency Name', 'State', 'Location', 'Department Type', 'Cameras Reported', '30-Day Vehicle Captures', 'Searches Conducted', 'Hotlist Alerts', 'Public Search Audit', 'Shared Agency Count', 'Transparency Portal URL' ]; const rows = list.map(p => [ `"${(p.name || '').replace(/"/g, '""')}"`, `"${p.state || ''}"`, `"${(p.location || '').replace(/"/g, '""')}"`, `"${p.dept_type || ''}"`, p.cameras || 0, p.vehicles_captured || 0, p.searches || 0, p.hotlist_hits || 0, p.has_search_audit ? 'Yes' : 'No', p.shared_count || 0, `"${p.url || ''}"` ]); const csvContent = [headers.join(','), ...rows.map(r => r.join(','))].join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); const timestamp = new Date().toISOString().slice(0, 10); link.setAttribute('href', url); link.setAttribute('download', `flock_transparency_portals_${state.selectedState.toLowerCase()}_${timestamp}.csv`); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); showToast(`Exported ${list.length.toLocaleString()} portals to CSV.`, 'success'); } // --- TOAST NOTIFICATIONS --- function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = 'toast'; let icon = 'ℹ️'; if (type === 'success') icon = 'βœ“'; if (type === 'error') icon = '⚠️'; if (type === 'warning') icon = '⚑'; toast.innerHTML = `${icon}${escapeHtml(message)}`; el.toastContainer.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateY(8px)'; toast.style.transition = '0.25s ease'; setTimeout(() => toast.remove(), 250); }, 3200); } // --- VIEW MODE TOGGLE --- function updateViewMode() { if (state.currentView === 'cards') { el.portalsGrid.style.display = 'grid'; el.tableContainer.style.display = 'none'; el.viewCardsBtn.classList.add('active'); el.viewTableBtn.classList.remove('active'); el.viewCardsBtn.setAttribute('aria-checked', 'true'); el.viewTableBtn.setAttribute('aria-checked', 'false'); } else { el.portalsGrid.style.display = 'none'; el.tableContainer.style.display = 'block'; el.viewCardsBtn.classList.remove('active'); el.viewTableBtn.classList.add('active'); el.viewCardsBtn.setAttribute('aria-checked', 'false'); el.viewTableBtn.setAttribute('aria-checked', 'true'); } localStorage.setItem('flock_dir_view', state.currentView); } // --- RESET FILTERS --- function resetAllFilters() { state.searchQuery = ''; state.selectedState = 'ALL'; state.selectedType = 'ALL'; state.auditOnly = false; state.sortBy = 'cameras-desc'; el.searchInput.value = ''; el.clearSearchBtn.style.display = 'none'; el.stateFilter.value = 'ALL'; el.typeFilter.value = 'ALL'; el.sortSelect.value = 'cameras-desc'; el.auditToggle.checked = false; renderStateChips(); applyFilters(); showToast('Filters reset to default view.'); } // --- URL HASH CHECK --- function checkUrlHash() { const hash = window.location.hash.replace('#', '').trim(); if (hash) { const match = state.allPortals.find(p => p.slug === hash); if (match) { openAgencyModal(hash); } } } // --- EVENT BINDINGS --- function bindEvents() { // Search input with debounce let debounceTimer; el.searchInput.addEventListener('input', e => { clearTimeout(debounceTimer); state.searchQuery = e.target.value; el.clearSearchBtn.style.display = state.searchQuery ? 'block' : 'none'; debounceTimer = setTimeout(() => { applyFilters(); }, 150); }); // Clear search button el.clearSearchBtn.addEventListener('click', () => { el.searchInput.value = ''; state.searchQuery = ''; el.clearSearchBtn.style.display = 'none'; el.searchInput.focus(); applyFilters(); }); // Keyboard shortcut '/' to search window.addEventListener('keydown', e => { if (e.key === '/' && document.activeElement !== el.searchInput) { e.preventDefault(); el.searchInput.focus(); el.searchInput.select(); } }); // State dropdown filter el.stateFilter.addEventListener('change', e => { state.selectedState = e.target.value; renderStateChips(); applyFilters(); }); // State chips click el.stateChipsScroll.addEventListener('click', e => { const chip = e.target.closest('.state-chip'); if (!chip) return; const st = chip.getAttribute('data-state'); if (!st) return; state.selectedState = st; el.stateFilter.value = st; renderStateChips(); applyFilters(); }); // Department type filter el.typeFilter.addEventListener('change', e => { state.selectedType = e.target.value; applyFilters(); }); // Sort select el.sortSelect.addEventListener('change', e => { state.sortBy = e.target.value; applyFilters(); }); // Public Audit toggle el.auditToggle.addEventListener('change', e => { state.auditOnly = e.target.checked; applyFilters(); }); // Reset all filters button el.btnResetAll.addEventListener('click', resetAllFilters); // View toggle buttons el.viewCardsBtn.addEventListener('click', () => { state.currentView = 'cards'; updateViewMode(); renderDirectory(); }); el.viewTableBtn.addEventListener('click', () => { state.currentView = 'table'; updateViewMode(); renderDirectory(); }); // Export CSV button el.btnExportCsv.addEventListener('click', exportFilteredToCsv); // Card / Table click delegation for Inspect buttons document.addEventListener('click', e => { const btn = e.target.closest('[data-action="inspect"]'); if (btn) { const slug = btn.getAttribute('data-slug'); if (slug) openAgencyModal(slug); } }); // Pagination button clicks el.paginationControls.addEventListener('click', e => { const btn = e.target.closest('.pagination-btn'); if (!btn || btn.disabled) return; if (btn.id === 'pg-prev') { state.currentPage = Math.max(1, state.currentPage - 1); } else if (btn.id === 'pg-next') { const maxPage = Math.ceil(state.filteredPortals.length / state.pageSize); state.currentPage = Math.min(maxPage, state.currentPage + 1); } else { const pageNum = parseInt(btn.getAttribute('data-page'), 10); if (pageNum) state.currentPage = pageNum; } renderDirectory(); // Smooth scroll back to top of directory document.getElementById('directory-section').scrollIntoView({ behavior: 'smooth' }); }); // Modal Close button el.modalCloseBtn.addEventListener('click', closeAgencyModal); // Modal Click-Outside Backdrop Dismissal el.agencyModal.addEventListener('click', e => { const rect = el.agencyModal.getBoundingClientRect(); const isInDialog = ( rect.top <= e.clientY && e.clientY <= rect.top + rect.height && rect.left <= e.clientX && e.clientX <= rect.left + rect.width ); if (!isInDialog) { closeAgencyModal(); } }); // Modal ESC Key el.agencyModal.addEventListener('cancel', e => { e.preventDefault(); closeAgencyModal(); }); // Modal Sharing Partners live search input el.modalSharingSearch.addEventListener('input', e => { if (!state.activeModalPortal) return; const sharedIds = state.activeModalPortal.shared_org_ids || []; renderModalSharingTags(sharedIds, e.target.value); }); // Modal Copy Link Button el.modalCopyLinkBtn.addEventListener('click', () => { if (!state.activeModalPortal) return; const shareUrl = `${window.location.origin}${window.location.pathname}#${state.activeModalPortal.slug}`; navigator.clipboard.writeText(shareUrl).then(() => { showToast('Direct link copied to clipboard!', 'success'); }).catch(() => { showToast(`Link: ${shareUrl}`, 'info'); }); }); // Hash change event (forward/back browser navigation) window.addEventListener('hashchange', checkUrlHash); } // --- UTILITY HELPERS --- function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function formatNumberCompact(num) { if (num >= 1000000) { return (num / 1000000).toFixed(1) + 'M'; } if (num >= 1000) { return (num / 1000).toFixed(0) + 'K'; } return num.toLocaleString(); } // --- START APP ON DOM READY --- if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();