fix:continuously watch location

This commit is contained in:
2025-10-27 15:07:24 +01:00
parent fef7c160e2
commit 3b3ebc2873
6 changed files with 240 additions and 15 deletions

View File

@@ -4,7 +4,8 @@
locationActions,
locationStatus,
locationError,
isLocationLoading
isLocationLoading,
isWatching
} from '$lib/stores/location';
import { Skeleton } from './skeleton';
@@ -22,26 +23,45 @@
showLabel = true
}: Props = $props();
async function handleLocationClick() {
const result = await locationActions.getCurrentLocation({
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 300000
});
// Track if location watching is active from the derived store
if (!result && $locationError) {
toast.error($locationError.message);
async function handleLocationClick() {
if ($isWatching) {
// Stop watching if currently active
locationActions.stopWatching();
toast.success('Location watching stopped');
} else {
// Try to get current location first, then start watching
const result = await locationActions.getCurrentLocation({
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 300000
});
if (result) {
// Start watching for continuous updates
locationActions.startWatching({
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 60000 // Update every minute
});
toast.success('Location watching started');
} else if ($locationError) {
toast.error($locationError.message);
}
}
}
const buttonText = $derived(() => {
if ($isLocationLoading) return 'Finding location...';
if ($locationStatus === 'success') return 'Update location';
if ($isWatching) return 'Stop watching location';
if ($locationStatus === 'success') return 'Watch location';
return 'Find my location';
});
const iconClass = $derived(() => {
if ($isLocationLoading) return 'loading';
if ($isWatching) return 'watching';
if ($locationStatus === 'success') return 'success';
if ($locationStatus === 'error') return 'error';
return 'default';
@@ -59,6 +79,22 @@
<div class="loading-skeleton">
<Skeleton class="h-5 w-5 rounded-full" />
</div>
{:else if $isWatching}
<svg viewBox="0 0 24 24" class="watching-icon">
<path
d="M12,11.5A2.5,2.5 0 0,1 9.5,9A2.5,2.5 0 0,1 12,6.5A2.5,2.5 0 0,1 14.5,9A2.5,2.5 0 0,1 12,11.5M12,2A7,7 0 0,0 5,9C5,14.25 12,22 12,22C12,22 19,14.25 19,9A7,7 0 0,0 12,2Z"
/>
<circle
cx="12"
cy="9"
r="15"
stroke="currentColor"
stroke-width="1"
fill="none"
opacity="0.3"
class="pulse-ring"
/>
</svg>
{:else if $locationStatus === 'success'}
<svg viewBox="0 0 24 24">
<path
@@ -201,6 +237,11 @@
fill: #10b981;
}
.icon.watching svg {
color: #f59e0b;
fill: #f59e0b;
}
.icon.error svg {
color: #ef4444;
fill: #ef4444;
@@ -219,6 +260,25 @@
}
}
.watching-icon .pulse-ring {
animation: pulse-ring 2s infinite;
}
@keyframes pulse-ring {
0% {
transform: scale(0.5);
opacity: 0.5;
}
50% {
transform: scale(1);
opacity: 0.3;
}
100% {
transform: scale(1.5);
opacity: 0;
}
}
.label {
white-space: nowrap;
}

View File

@@ -0,0 +1,81 @@
<script lang="ts">
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { locationActions, isWatching, coordinates } from '$lib/stores/location';
interface Props {
autoStart?: boolean;
enableHighAccuracy?: boolean;
timeout?: number;
maximumAge?: number;
}
let {
autoStart = true,
enableHighAccuracy = true,
timeout = 15000,
maximumAge = 60000
}: Props = $props();
// Location watching options
const watchOptions = {
enableHighAccuracy,
timeout,
maximumAge
};
onMount(() => {
if (!browser || !autoStart) return;
// Check if geolocation is supported
if (!navigator.geolocation) {
console.warn('Geolocation is not supported by this browser');
return;
}
// Check if we already have coordinates and aren't watching
if ($coordinates && !$isWatching) {
// Start watching immediately if we have previous coordinates
startLocationWatching();
return;
}
// If no coordinates, try to get current location first
if (!$coordinates) {
getCurrentLocationThenWatch();
}
});
async function getCurrentLocationThenWatch() {
try {
const result = await locationActions.getCurrentLocation(watchOptions);
if (result) {
// Successfully got location, now start watching
startLocationWatching();
}
} catch {
// If we can't get location due to permissions, don't auto-start watching
console.log('Could not get initial location, location watching not started automatically');
}
}
function startLocationWatching() {
if (!$isWatching) {
locationActions.startWatching(watchOptions);
}
}
// Cleanup function to stop watching when component is destroyed
function cleanup() {
if ($isWatching) {
locationActions.stopWatching();
}
}
// Stop watching when the component is destroyed
onMount(() => {
return cleanup;
});
</script>
<!-- This component doesn't render anything, it just manages location watching -->

View File

@@ -6,7 +6,8 @@
getMapCenter,
getMapZoom,
shouldZoomToLocation,
locationActions
locationActions,
isWatching
} from '$lib/stores/location';
import LocationButton from './LocationButton.svelte';
import { Skeleton } from '$lib/components/skeleton';
@@ -139,11 +140,14 @@
>
{#if $coordinates}
<Marker lngLat={[$coordinates.longitude, $coordinates.latitude]}>
<div class="location-marker">
<div class="marker-pulse"></div>
<div class="marker-outer">
<div class="marker-inner"></div>
<div class="location-marker" class:watching={$isWatching}>
<div class="marker-pulse" class:watching={$isWatching}></div>
<div class="marker-outer" class:watching={$isWatching}>
<div class="marker-inner" class:watching={$isWatching}></div>
</div>
{#if $isWatching}
<div class="watching-ring"></div>
{/if}
</div>
</Marker>
{/if}
@@ -243,6 +247,13 @@
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
:global(.marker-outer.watching) {
background: rgba(245, 158, 11, 0.2);
border-color: #f59e0b;
box-shadow: 0 0 0 4px rgba(245, 158, 11, 0.1);
}
:global(.marker-inner) {
@@ -250,6 +261,12 @@
height: 8px;
background: #2563eb;
border-radius: 50%;
transition: all 0.3s ease;
}
:global(.marker-inner.watching) {
background: #f59e0b;
animation: pulse-glow 2s infinite;
}
:global(.marker-pulse) {
@@ -263,6 +280,22 @@
animation: pulse 2s infinite;
}
:global(.marker-pulse.watching) {
border-color: rgba(245, 158, 11, 0.6);
animation: pulse-watching 1.5s infinite;
}
:global(.watching-ring) {
position: absolute;
top: -8px;
left: -8px;
width: 36px;
height: 36px;
border: 2px solid rgba(245, 158, 11, 0.4);
border-radius: 50%;
animation: expand-ring 3s infinite;
}
@keyframes pulse {
0% {
transform: scale(1);
@@ -274,6 +307,48 @@
}
}
@keyframes pulse-watching {
0% {
transform: scale(1);
opacity: 0.8;
}
50% {
transform: scale(1.5);
opacity: 0.4;
}
100% {
transform: scale(2);
opacity: 0;
}
}
@keyframes pulse-glow {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.2);
}
}
@keyframes expand-ring {
0% {
transform: scale(1);
opacity: 0.6;
}
50% {
transform: scale(1.3);
opacity: 0.3;
}
100% {
transform: scale(1.6);
opacity: 0;
}
}
/* Find marker styles */
:global(.find-marker) {
width: 40px;

View File

@@ -8,6 +8,7 @@ export { default as Header } from './components/Header.svelte';
export { default as Modal } from './components/Modal.svelte';
export { default as Map } from './components/Map.svelte';
export { default as LocationButton } from './components/LocationButton.svelte';
export { default as LocationManager } from './components/LocationManager.svelte';
export { default as FindCard } from './components/FindCard.svelte';
export { default as FindsList } from './components/FindsList.svelte';
@@ -24,6 +25,7 @@ export {
locationError,
isLocationLoading,
hasLocationAccess,
isWatching,
getMapCenter,
getMapZoom
} from './stores/location';

View File

@@ -43,6 +43,7 @@ export const shouldZoomToLocation = derived(
locationStore,
($location) => $location.shouldZoomToLocation
);
export const isWatching = derived(locationStore, ($location) => $location.isWatching);
// Location actions
export const locationActions = {

View File

@@ -5,6 +5,7 @@
import { page } from '$app/state';
import { Toaster } from '$lib/components/sonner/index.js';
import { Skeleton } from '$lib/components/skeleton';
import LocationManager from '$lib/components/LocationManager.svelte';
let { children, data } = $props();
let isLoginRoute = $derived(page.url.pathname.startsWith('/login'));
@@ -32,6 +33,11 @@
<Toaster />
<!-- Auto-start location watching for authenticated users -->
{#if data?.user && !isLoginRoute}
<LocationManager autoStart={true} />
{/if}
{#if showHeader && data.user}
<Header user={data.user} />
{:else if isLoading}