feat(directory): list specific camera nodes with cross-streets and photos in city search
This commit is contained in:
+196
@@ -1277,6 +1277,202 @@ body {
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
/* Directory Specific Camera Nodes & Hardware Images Grid */
|
||||
.directory-cameras-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.dir-cam-card {
|
||||
background: rgba(15, 23, 42, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
border: 1px solid rgba(56, 189, 248, 0.25);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.dir-cam-card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: #38bdf8;
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5), 0 0 20px rgba(6, 182, 212, 0.2);
|
||||
}
|
||||
|
||||
.dir-cam-image-container {
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
background: #090d16;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.dir-cam-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.dir-cam-card:hover .dir-cam-img {
|
||||
transform: scale(1.04);
|
||||
}
|
||||
|
||||
.dir-cam-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
background: linear-gradient(135deg, rgba(30, 41, 59, 0.6), rgba(15, 23, 42, 0.9));
|
||||
color: #94a3b8;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dir-cam-placeholder-icon {
|
||||
font-size: 2.2rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.dir-cam-badge {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
padding: 4px 10px;
|
||||
border-radius: 9999px;
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.dir-cam-badge.verified {
|
||||
background: rgba(16, 185, 129, 0.85);
|
||||
color: #fff;
|
||||
border: 1px solid #34d399;
|
||||
}
|
||||
|
||||
.dir-cam-badge.unverified {
|
||||
background: rgba(245, 158, 11, 0.85);
|
||||
color: #fff;
|
||||
border: 1px solid #fcd34d;
|
||||
}
|
||||
|
||||
.dir-cam-body {
|
||||
padding: 18px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dir-cam-intersection {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin-bottom: 6px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.dir-cam-agency-meta {
|
||||
font-size: 0.82rem;
|
||||
color: #38bdf8;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.dir-cam-specs-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 8px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dir-cam-spec-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dir-cam-spec-label {
|
||||
font-size: 0.65rem;
|
||||
color: #94a3b8;
|
||||
font-family: var(--font-mono);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.dir-cam-spec-val {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.dir-cam-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1.2fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.btn-dir-cam-jump {
|
||||
background: linear-gradient(135deg, #06b6d4, #0284c7);
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 9px 12px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-dir-cam-jump:hover {
|
||||
background: linear-gradient(135deg, #0891b2, #0369a1);
|
||||
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.4);
|
||||
}
|
||||
|
||||
.btn-dir-cam-details {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 9px 12px;
|
||||
font-size: 0.82rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-dir-cam-details:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
/* Portals Grid */
|
||||
.portals-grid {
|
||||
display: grid;
|
||||
|
||||
+29
-5
@@ -28,7 +28,7 @@
|
||||
<meta name="apple-mobile-web-app-title" content="FlockRadar">
|
||||
|
||||
<!-- Main Stylesheet -->
|
||||
<link rel="stylesheet" href="css/styles.css?v=10">
|
||||
<link rel="stylesheet" href="css/styles.css?v=11">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -487,6 +487,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Specific Camera Nodes & Hardware Images for Searched City / Location -->
|
||||
<section id="directory-cameras-section" class="directory-cameras-section" style="display: none; margin-bottom: 36px;">
|
||||
<div class="dir-cam-header" style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; padding: 14px 18px; background: rgba(6, 182, 212, 0.08); border: 1px solid rgba(6, 182, 212, 0.25); border-radius: 14px;">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<span style="font-size: 1.4rem;">📸</span>
|
||||
<div>
|
||||
<h3 style="margin: 0; font-size: 1.1rem; color: #38bdf8; font-family: var(--font-heading);">
|
||||
Identified Camera Nodes in <span id="dir-cam-query-title">City</span>
|
||||
</h3>
|
||||
<span id="dir-cam-subtitle" style="font-size: 0.8rem; color: #94a3b8;">
|
||||
Mapped intersections, cross streets, and physical hardware photos
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<span id="dir-cam-badge-count" style="background: #06b6d4; color: #07090e; font-weight: 800; font-size: 0.78rem; padding: 4px 12px; border-radius: 9999px;">
|
||||
0 Cameras
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="directory-cameras-grid" class="directory-cameras-grid">
|
||||
<!-- Dynamically populated camera cards with photos, cross streets, and jump-to-radar buttons -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cards Grid View -->
|
||||
<div class="portals-grid" id="portals-grid" aria-live="polite"></div>
|
||||
|
||||
@@ -1031,10 +1055,10 @@
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
|
||||
<!-- Data & Application Logic with Cache Busting -->
|
||||
<script src="data/portals.js?v=10"></script>
|
||||
<script src="js/auth.js?v=10"></script>
|
||||
<script src="js/radar.js?v=10"></script>
|
||||
<script src="js/app.js?v=10"></script>
|
||||
<script src="data/portals.js?v=11"></script>
|
||||
<script src="js/auth.js?v=11"></script>
|
||||
<script src="js/radar.js?v=11"></script>
|
||||
<script src="js/app.js?v=11"></script>
|
||||
|
||||
<!-- Service Worker Instant Cache Invalidation -->
|
||||
<script>
|
||||
|
||||
@@ -75,6 +75,13 @@
|
||||
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')
|
||||
};
|
||||
@@ -91,6 +98,58 @@
|
||||
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') {
|
||||
@@ -273,8 +332,140 @@
|
||||
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);
|
||||
return nameMatch || agencyMatch || slugMatch || notesMatch;
|
||||
});
|
||||
} else if (state.selectedState && state.selectedState !== 'ALL') {
|
||||
matchingCams = allCams.filter(c => {
|
||||
if (state.selectedState === 'CA') {
|
||||
return c.id.includes('plm') || c.id.includes('lan') || c.id.includes('cal') || (c.agency && c.agency.includes('LASD'));
|
||||
}
|
||||
if (state.selectedState === 'KY') return c.id.includes('lex');
|
||||
if (state.selectedState === 'GA') return c.id.includes('atl');
|
||||
if (state.selectedState === 'TX') return c.id.includes('dfw');
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
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 badgeHtml = meta.isContested
|
||||
? `<span class="dir-cam-badge unverified">🟠 CONTESTED</span>`
|
||||
: (hasPhoto ? `<span class="dir-cam-badge verified">🟢 PHOTO VERIFIED</span>` : `<span class="dir-cam-badge unverified">⚠️ FLOCK RECORDED</span>`);
|
||||
|
||||
const imageContainerHtml = hasPhoto
|
||||
? `<div class="dir-cam-image-container">
|
||||
<img src="${photoUrl}" class="dir-cam-img" alt="Camera at ${escapeHtml(cam.name)}" onclick="window.open('${photoUrl}', '_blank')" title="Click to view full photo proof">
|
||||
${badgeHtml}
|
||||
</div>`
|
||||
: `<div class="dir-cam-image-container">
|
||||
<div class="dir-cam-placeholder">
|
||||
<span class="dir-cam-placeholder-icon">📸</span>
|
||||
<strong style="font-size: 0.82rem; color: #cbd5e1;">Hardware Photo Pending</strong>
|
||||
<span style="font-size: 0.72rem; color: #94a3b8;">Mounted on utility / signal pole</span>
|
||||
</div>
|
||||
${badgeHtml}
|
||||
</div>`;
|
||||
|
||||
return `
|
||||
<article class="dir-cam-card" id="dir-cam-node-${escapeHtml(cam.id)}">
|
||||
${imageContainerHtml}
|
||||
<div class="dir-cam-body">
|
||||
<div>
|
||||
<h4 class="dir-cam-intersection">${escapeHtml(cam.name)}</h4>
|
||||
<div class="dir-cam-agency-meta">
|
||||
<span>🏛️ ${escapeHtml(cam.agency || 'Local Law Enforcement')}</span>
|
||||
</div>
|
||||
|
||||
<div class="dir-cam-specs-row">
|
||||
<div class="dir-cam-spec-item">
|
||||
<span class="dir-cam-spec-label">HARDWARE</span>
|
||||
<span class="dir-cam-spec-val">${escapeHtml(cam.type || 'Falcon Solar ALPR')}</span>
|
||||
</div>
|
||||
<div class="dir-cam-spec-item">
|
||||
<span class="dir-cam-spec-label">MONTHLY SCANS</span>
|
||||
<span class="dir-cam-spec-val" style="color: #34d399;">${escapeHtml(cam.captures30d ? `${cam.captures30d}` : '300,000+')}</span>
|
||||
</div>
|
||||
<div class="dir-cam-spec-item">
|
||||
<span class="dir-cam-spec-label">COORDINATES</span>
|
||||
<span class="dir-cam-spec-val" style="font-family: var(--font-mono); font-size: 0.75rem;">${cam.lat.toFixed(4)}, ${cam.lng.toFixed(4)}</span>
|
||||
</div>
|
||||
<div class="dir-cam-spec-item">
|
||||
<span class="dir-cam-spec-label">CONSENSUS</span>
|
||||
<span class="dir-cam-spec-val" style="color: #38bdf8;">👍 ${meta.confirmations || 1} Confirmed</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dir-cam-actions">
|
||||
<button type="button" class="btn-dir-cam-jump" onclick="window.jumpToRadarCamera('${escapeHtml(cam.id)}')">
|
||||
<span>🗺️</span>
|
||||
<span>View on Live Radar</span>
|
||||
</button>
|
||||
<button type="button" class="btn-dir-cam-details" onclick="window.openCameraHardwareProof('${escapeHtml(cam.id)}')">
|
||||
<span>📸</span>
|
||||
<span>Camera Info</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
}).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();
|
||||
|
||||
+18
@@ -269,6 +269,7 @@
|
||||
zoomControl: false,
|
||||
attributionControl: false
|
||||
});
|
||||
window.flockMap = map;
|
||||
|
||||
// High-Resolution Radar Dark Tiles (Zero API Key, Zero Watermarks, Sub-Meter Street Clarity up to Zoom 19)
|
||||
darkTileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
@@ -2089,6 +2090,23 @@
|
||||
window.openAgencyModal(slug);
|
||||
}
|
||||
};
|
||||
|
||||
// Global camera list provider for Directory integration
|
||||
window.getFlockAllCameras = function() {
|
||||
return [...allCameras];
|
||||
};
|
||||
|
||||
// Global jump from Directory straight to specific camera on Radar
|
||||
window.jumpToRadarCamera = function(camId) {
|
||||
if (el.navMapBtn) el.navMapBtn.click();
|
||||
setTimeout(() => {
|
||||
const cam = allCameras.find(c => c.id === camId);
|
||||
if (cam && map) {
|
||||
map.flyTo([cam.lat, cam.lng], 16, { animate: true, duration: 1.0 });
|
||||
}
|
||||
openCameraHardwareProof(camId);
|
||||
}, 150);
|
||||
};
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// Service Worker for FlockRadar Offline Driving & Live Updates
|
||||
const CACHE_NAME = 'flockradar-v10';
|
||||
const CACHE_NAME = 'flockradar-v11';
|
||||
const ASSETS_TO_CACHE = [
|
||||
'./',
|
||||
'./index.html',
|
||||
'./css/styles.css?v=10',
|
||||
'./js/app.js?v=10',
|
||||
'./js/auth.js?v=10',
|
||||
'./js/radar.js?v=10',
|
||||
'./data/portals.js?v=10',
|
||||
'./css/styles.css?v=11',
|
||||
'./js/app.js?v=11',
|
||||
'./js/auth.js?v=11',
|
||||
'./js/radar.js?v=11',
|
||||
'./data/portals.js?v=11',
|
||||
'./manifest.json'
|
||||
];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user