fix:continuously watch location
This commit is contained in:
@@ -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();
|
||||||
|
|
||||||
async function handleLocationClick() {
|
// Track if location watching is active from the derived store
|
||||||
const result = await locationActions.getCurrentLocation({
|
|
||||||
enableHighAccuracy: true,
|
|
||||||
timeout: 15000,
|
|
||||||
maximumAge: 300000
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!result && $locationError) {
|
async function handleLocationClick() {
|
||||||
toast.error($locationError.message);
|
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(() => {
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
81
src/lib/components/LocationManager.svelte
Normal file
81
src/lib/components/LocationManager.svelte
Normal 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 -->
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user