diff --git a/src/lib/components/LocationButton.svelte b/src/lib/components/LocationButton.svelte index 3d74d1f..22fee5d 100644 --- a/src/lib/components/LocationButton.svelte +++ b/src/lib/components/LocationButton.svelte @@ -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 @@
+ {:else if $isWatching} + + + + {:else if $locationStatus === 'success'} + 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; + }); + + + diff --git a/src/lib/components/Map.svelte b/src/lib/components/Map.svelte index 22a747e..3861d58 100644 --- a/src/lib/components/Map.svelte +++ b/src/lib/components/Map.svelte @@ -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} -
-
-
-
+
+
+
+
+ {#if $isWatching} +
+ {/if}
{/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; diff --git a/src/lib/index.ts b/src/lib/index.ts index af6f7a1..17866c7 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -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'; diff --git a/src/lib/stores/location.ts b/src/lib/stores/location.ts index ca943dd..a70fcc3 100644 --- a/src/lib/stores/location.ts +++ b/src/lib/stores/location.ts @@ -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 = { diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 495dfdd..a0ffc26 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -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 @@ + +{#if data?.user && !isLoginRoute} + +{/if} + {#if showHeader && data.user}
{:else if isLoading}