3 Commits

Author SHA1 Message Date
2d7efb03fb Update version to 1.1.0 in package.json and package-lock.json
All checks were successful
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 18s
CI / check (push) Successful in 22s
CI / build (push) Successful in 39s
2025-12-24 21:13:20 -06:00
191ed6b0e1 Improve box selection functionality in map component
- Improved box selection mode logic by ensuring proper state checks and event handling.
- Added conditions to prevent actions when the selection mode is not active or when necessary elements are missing.
- Updated the UI to include a tooltip for better user guidance during box selection.
- Refactored code for clarity and maintainability, ensuring a smoother user experience.
2025-12-24 21:13:08 -06:00
57187e7ed4 Add tooltip for box selection feature in map component
All checks were successful
OSV-Scanner Scheduled Scan / scan-scheduled (push) Successful in 19s
CI / check (push) Successful in 22s
CI / build (push) Successful in 40s
- Introduced a tooltip that guides users to draw a box for camera searches, displayed conditionally based on local storage state.
- Implemented tooltip positioning logic and dismiss functionality.
- Updated the Svelte component to enhance user interaction and experience.
2025-12-24 21:09:29 -06:00
3 changed files with 99 additions and 21 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "surveilled",
"version": "1.0.0",
"version": "1.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "surveilled",
"version": "1.0.0",
"version": "1.1.0",
"dependencies": {
"autoprefixer": "^10.4.23",
"lucide-svelte": "^0.562.0",

View File

@@ -1,7 +1,7 @@
{
"name": "surveilled",
"private": true,
"version": "1.0.0",
"version": "1.1.0",
"type": "module",
"scripts": {
"dev": "VITE_APP_VERSION=$(node -p \"require('./package.json').version\") vite dev",

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { onMount, onDestroy } from 'svelte';
import { onMount, onDestroy, tick } from 'svelte';
import Map from 'ol/Map.js';
import type MapBrowserEvent from 'ol/MapBrowserEvent.js';
import { Style, Stroke, Fill } from 'ol/style.js';
@@ -88,6 +88,9 @@
let locationSearchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
let selectionBoxCenterPixel: [number, number] | null = null;
let footerCollapsed = false;
let showBoxTooltip = false;
let boxSelectButton: HTMLButtonElement;
let boxTooltipPosition: { left: number; top: number } | null = null;
let cameraSource: VectorSource;
let clusterSource: Cluster;
@@ -203,6 +206,15 @@
restoreStateFromUrl();
const hasSeenTooltip = localStorage.getItem('surveilled-box-tooltip-seen');
if (!hasSeenTooltip) {
setTimeout(async () => {
showBoxTooltip = true;
await tick();
updateTooltipPosition();
}, 500);
}
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
@@ -424,19 +436,48 @@
}
}
function updateTooltipPosition() {
if (!boxSelectButton) return;
const toolbar = boxSelectButton.closest('.toolbar');
if (!toolbar) return;
const toolbarContainer = toolbar.parentElement;
if (!toolbarContainer) return;
const buttonRect = boxSelectButton.getBoundingClientRect();
const containerRect = toolbarContainer.getBoundingClientRect();
boxTooltipPosition = {
left: buttonRect.left - containerRect.left + buttonRect.width / 2,
top: buttonRect.bottom - containerRect.top + 8,
};
}
function dismissBoxTooltip() {
showBoxTooltip = false;
localStorage.setItem('surveilled-box-tooltip-seen', 'true');
}
function toggleBoxSelectMode() {
if (isBoxSelectMode) {
disableBoxSelectMode();
} else {
enableBoxSelectMode();
}
dismissBoxTooltip();
}
function enableBoxSelectMode() {
if (isBoxSelectMode || !map || !selectionLayer) return;
if (!map || !selectionLayer) return;
const source = selectionLayer.getSource();
if (!source) return;
if (boxMoveHandler) {
unByKey(boxMoveHandler);
boxMoveHandler = null;
}
if (isBoxSelectMode) {
return;
}
isBoxSelectMode = true;
source.clear();
boxStartPoint = null;
@@ -456,11 +497,16 @@
}
function handleBoxClick(event: MapBrowserEvent<PointerEvent | KeyboardEvent | WheelEvent>) {
if (!map || !selectionLayer) return;
if (!map || !selectionLayer || !isBoxSelectMode) return;
const source = selectionLayer.getSource();
if (!source) return;
const coordinate = event.coordinate;
if (!coordinate) return;
event.originalEvent?.preventDefault?.();
event.originalEvent?.stopPropagation?.();
const lonLat = toLonLat(coordinate);
const lonLatTuple: [number, number] = [lonLat[0], lonLat[1]];
@@ -480,7 +526,7 @@
}
function updateBoxPreview(endCoordinate: number[]) {
if (!map || !selectionLayer || !boxStartPoint) return;
if (!map || !selectionLayer || !boxStartPoint || !isBoxSelectMode) return;
const source = selectionLayer.getSource();
if (!source) return;
@@ -515,9 +561,6 @@
width: 2.5,
lineDash: [5, 3],
}),
fill: new Fill({
color: 'rgba(239, 68, 68, 0.18)',
}),
})
);
source.clear();
@@ -551,8 +594,7 @@
}
function disableBoxSelectMode(keepSelection = false) {
if (!isBoxSelectMode || !map) return;
isBoxSelectMode = false;
if (!map) return;
if (boxMoveHandler) {
unByKey(boxMoveHandler);
@@ -572,8 +614,12 @@
if (map.getTargetElement()) {
map.getTargetElement().style.cursor = '';
}
setStatusMessage('');
updateUrlState();
if (isBoxSelectMode) {
isBoxSelectMode = false;
setStatusMessage('');
updateUrlState();
}
}
function handleRefresh() {
@@ -946,13 +992,21 @@
<div
class="toolbar bg-bg-secondary/80 backdrop-blur-sm border border-border-color rounded-lg px-2 py-1.5 flex items-center gap-1 shadow-lg"
>
<button
class="toolbar-btn {isBoxSelectMode ? 'active' : ''}"
title="Select Area (B)"
on:click={toggleBoxSelectMode}
>
<Square size={16} />
</button>
<div class="relative">
{#if showBoxTooltip}
<div
class="absolute -inset-1 rounded-lg border-2 border-accent-red animate-pulse pointer-events-none z-10"
></div>
{/if}
<button
bind:this={boxSelectButton}
class="toolbar-btn {isBoxSelectMode ? 'active' : ''} relative"
title="Select Area (B)"
on:click={toggleBoxSelectMode}
>
<Square size={16} />
</button>
</div>
<button class="toolbar-btn" title="Refresh (R)" on:click={handleRefresh}>
<RefreshCw size={16} />
</button>
@@ -983,6 +1037,30 @@
<option value="satellite">Satellite</option>
</select>
</div>
{#if showBoxTooltip && boxTooltipPosition}
<div
class="absolute z-[1300] pointer-events-none"
style="left: {boxTooltipPosition.left}px; top: {boxTooltipPosition.top}px; transform: translateX(-50%);"
>
<div
class="bg-bg-secondary border border-border-color rounded-lg px-3 py-2 shadow-lg text-sm text-text-primary relative"
>
<div
class="absolute -top-2 left-1/2 -translate-x-1/2 w-0 h-0 border-l-4 border-r-4 border-b-4 border-l-transparent border-r-transparent border-b-border-color"
></div>
<div
class="absolute -top-[7px] left-1/2 -translate-x-1/2 w-0 h-0 border-l-[7px] border-r-[7px] border-b-[7px] border-l-transparent border-r-transparent border-b-bg-secondary"
></div>
<div class="relative z-10">Click here to draw a box and search for cameras</div>
<button
class="absolute top-1 right-1 text-text-secondary hover:text-text-primary pointer-events-auto"
on:click={dismissBoxTooltip}
>
×
</button>
</div>
</div>
{/if}
</div>
<button
class="sm:hidden fixed bottom-4 left-4 z-[1100] bg-bg-secondary border border-border-color rounded-full px-3 py-2 text-xs font-semibold text-text-primary shadow-lg"