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, locationActions,
locationStatus, locationStatus,
locationError, locationError,
isLocationLoading isLocationLoading,
isWatching
} from '$lib/stores/location'; } from '$lib/stores/location';
import { Skeleton } from './skeleton'; import { Skeleton } from './skeleton';
@@ -22,26 +23,45 @@
showLabel = true showLabel = true
}: Props = $props(); }: Props = $props();
// Track if location watching is active from the derived store
async function handleLocationClick() { 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({ const result = await locationActions.getCurrentLocation({
enableHighAccuracy: true, enableHighAccuracy: true,
timeout: 15000, timeout: 15000,
maximumAge: 300000 maximumAge: 300000
}); });
if (!result && $locationError) { 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); toast.error($locationError.message);
} }
} }
}
const buttonText = $derived(() => { const buttonText = $derived(() => {
if ($isLocationLoading) return 'Finding location...'; 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'; return 'Find my location';
}); });
const iconClass = $derived(() => { const iconClass = $derived(() => {
if ($isLocationLoading) return 'loading'; if ($isLocationLoading) return 'loading';
if ($isWatching) return 'watching';
if ($locationStatus === 'success') return 'success'; if ($locationStatus === 'success') return 'success';
if ($locationStatus === 'error') return 'error'; if ($locationStatus === 'error') return 'error';
return 'default'; return 'default';
@@ -59,6 +79,22 @@
<div class="loading-skeleton"> <div class="loading-skeleton">
<Skeleton class="h-5 w-5 rounded-full" /> <Skeleton class="h-5 w-5 rounded-full" />
</div> </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'} {:else if $locationStatus === 'success'}
<svg viewBox="0 0 24 24"> <svg viewBox="0 0 24 24">
<path <path
@@ -201,6 +237,11 @@
fill: #10b981; fill: #10b981;
} }
.icon.watching svg {
color: #f59e0b;
fill: #f59e0b;
}
.icon.error svg { .icon.error svg {
color: #ef4444; color: #ef4444;
fill: #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 { .label {
white-space: nowrap; 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, getMapCenter,
getMapZoom, getMapZoom,
shouldZoomToLocation, shouldZoomToLocation,
locationActions locationActions,
isWatching
} from '$lib/stores/location'; } from '$lib/stores/location';
import LocationButton from './LocationButton.svelte'; import LocationButton from './LocationButton.svelte';
import { Skeleton } from '$lib/components/skeleton'; import { Skeleton } from '$lib/components/skeleton';
@@ -139,11 +140,14 @@
> >
{#if $coordinates} {#if $coordinates}
<Marker lngLat={[$coordinates.longitude, $coordinates.latitude]}> <Marker lngLat={[$coordinates.longitude, $coordinates.latitude]}>
<div class="location-marker"> <div class="location-marker" class:watching={$isWatching}>
<div class="marker-pulse"></div> <div class="marker-pulse" class:watching={$isWatching}></div>
<div class="marker-outer"> <div class="marker-outer" class:watching={$isWatching}>
<div class="marker-inner"></div> <div class="marker-inner" class:watching={$isWatching}></div>
</div> </div>
{#if $isWatching}
<div class="watching-ring"></div>
{/if}
</div> </div>
</Marker> </Marker>
{/if} {/if}
@@ -243,6 +247,13 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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) { :global(.marker-inner) {
@@ -250,6 +261,12 @@
height: 8px; height: 8px;
background: #2563eb; background: #2563eb;
border-radius: 50%; border-radius: 50%;
transition: all 0.3s ease;
}
:global(.marker-inner.watching) {
background: #f59e0b;
animation: pulse-glow 2s infinite;
} }
:global(.marker-pulse) { :global(.marker-pulse) {
@@ -263,6 +280,22 @@
animation: pulse 2s infinite; 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 { @keyframes pulse {
0% { 0% {
transform: scale(1); 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 */ /* Find marker styles */
:global(.find-marker) { :global(.find-marker) {
width: 40px; 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 Modal } from './components/Modal.svelte';
export { default as Map } from './components/Map.svelte'; export { default as Map } from './components/Map.svelte';
export { default as LocationButton } from './components/LocationButton.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 FindCard } from './components/FindCard.svelte';
export { default as FindsList } from './components/FindsList.svelte'; export { default as FindsList } from './components/FindsList.svelte';
@@ -24,6 +25,7 @@ export {
locationError, locationError,
isLocationLoading, isLocationLoading,
hasLocationAccess, hasLocationAccess,
isWatching,
getMapCenter, getMapCenter,
getMapZoom getMapZoom
} from './stores/location'; } from './stores/location';

View File

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

View File

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