Skip to content

Commit

Permalink
add useGelocation
Browse files Browse the repository at this point in the history
  • Loading branch information
huntabyte committed Dec 5, 2024
1 parent 0f52670 commit b788207
Show file tree
Hide file tree
Showing 11 changed files with 198 additions and 3 deletions.
5 changes: 5 additions & 0 deletions .changeset/gentle-plants-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"runed": minor
---

change: handle boolean conversion within `IsSupported` to improve DX
5 changes: 5 additions & 0 deletions .changeset/young-comics-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"runed": minor
---

feat: `useGeolocation`
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default tseslint.config(
],
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-empty-object-type": "off",
"prefer-const": "off",
},
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
export class IsSupported {
#current: boolean = $state(false);

constructor(predicate: () => boolean) {
constructor(predicate: () => unknown) {
$effect(() => {
this.#current = predicate();
console.log("predicate", predicate());
this.#current = Boolean(predicate());
});
}

Expand Down
1 change: 1 addition & 0 deletions packages/runed/src/lib/utilities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@ export * from "./useIntersectionObserver/index.js";
export * from "./IsFocusWithin/index.js";
export * from "./FiniteStateMachine/index.js";
export * from "./PersistedState/index.js";
export * from "./useGeolocation/index.js";
1 change: 1 addition & 0 deletions packages/runed/src/lib/utilities/useGeolocation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./useGeolocation.svelte.js";
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { IsSupported } from "../IsSupported/IsSupported.svelte.js";

type UseGeolocationOptions = Partial<PositionOptions> & {
/**
* Whether to start the watcher immediately upon creation. If set to `false`, the watcher
* will only start tracking the position when `resume()` is called.
*
* @defaultValue true
*/
immediate?: boolean;
};

type WritableProperties<T> = {
-readonly [P in keyof T]: T[P];
};

/**
* Reactive access to the browser's [Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API).
*/
export function useGeolocation(options: UseGeolocationOptions = {}) {
const {
enableHighAccuracy = true,
maximumAge = 30000,
timeout = 27000,
immediate = true,
} = options;

const isSupported = new IsSupported(() => navigator && "geolocation" in navigator);

let locatedAt = $state<number | null>(null);
let error = $state.raw<GeolocationPositionError | null>(null);
let coords = $state<WritableProperties<Omit<GeolocationPosition["coords"], "toJSON">>>({
accuracy: 0,
latitude: Number.POSITIVE_INFINITY,
longitude: Number.POSITIVE_INFINITY,
altitude: null,
altitudeAccuracy: null,
heading: null,
speed: null,
});
let isPaused = $state(false);

function updatePosition(position: GeolocationPosition) {
locatedAt = position.timestamp;
coords.accuracy = position.coords.accuracy;
coords.altitude = position.coords.altitude;
coords.altitudeAccuracy = position.coords.altitudeAccuracy;
coords.heading = position.coords.heading;
coords.latitude = position.coords.latitude;
coords.longitude = position.coords.longitude;
coords.speed = position.coords.speed;
}

let watcher: number;

function resume() {
if (!isSupported.current) return;
watcher = navigator!.geolocation.watchPosition(updatePosition, (err) => (error = err), {
enableHighAccuracy,
maximumAge,
timeout,
});
isPaused = false;
}

function pause() {
if (watcher && navigator) navigator.geolocation.clearWatch(watcher);
isPaused = true;
}

$effect(() => {
if (immediate) resume();
return () => pause();
});

return {
get isSupported() {
return isSupported.current;
},
get coords() {
return coords;
},
get locatedAt() {
return locatedAt;
},
get error() {
return error;
},
get isPaused() {
return isPaused;
},
resume,
pause,
};
}
2 changes: 1 addition & 1 deletion scripts/add-utility.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ fs.writeFileSync(
`
<script lang="ts">
import { ${utilName} } from 'runed';
import { DemoContainer } from '@svecodocs/ui';
import { DemoContainer } from '@svecodocs/kit';
</script>
<DemoContainer>
Expand Down
8 changes: 8 additions & 0 deletions sites/docs/src/content/utilities/is-supported.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@ category: Utilities
}
</script>
```

## Type Definition

```ts
class IsSupported {
readonly current: boolean;
}
```
61 changes: 61 additions & 0 deletions sites/docs/src/content/utilities/use-geolocation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
title: useGeolocation
description: Reactive access to the browser's Geolocation API.
category: Browser
---

<script>
import Demo from '$lib/components/demos/use-geolocation.svelte';
</script>

`useGeolocation` is a reactive wrapper around the browser's
[Geolocation API](https://developer.mozilla.org/en-US/docs/Web/API/Geolocation_API).

## Demo

<Demo />

## Usage

```svelte
<script lang="ts">
import { useGeolocation } from "runed";
const location = useGeolocation();
</script>
<pre>Coords: {JSON.stringify(location.coords, null, 2)}</pre>
<pre>Located at: {location.locatedAt}</pre>
<pre>Error: {JSON.stringify(location.error, null, 2)}</pre>
<pre>Is Supported: {location.isSupported}</pre>
<button onclick={location.pause} disabled={location.isPaused}>Pause</button>
<button onclick={location.resume} disabled={!location.isPaused}>Resume</button>
```

## Type Definitions

```ts
type UseGeolocationOptions = Partial<PositionOptions> & {
/**
* Whether to start the watcher immediately upon creation.
* If set to `false`, the watcher will only start tracking the position when `resume()` is called.
*
* @defaultValue true
*/
immediate?: boolean;
};

type UseGeolocationReturn = {
readonly isSupported: boolean;
readonly coords: Omit<GeolocationPosition["coords"], "toJSON">;
readonly locatedAt: number | null;
readonly error: GeolocationPositionError | null;
readonly isPaused: boolean;
pause: () => void;
resume: () => void;
};
```

```
```
17 changes: 17 additions & 0 deletions sites/docs/src/lib/components/demos/use-geolocation.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script lang="ts">
import { useGeolocation } from "runed";
import { DemoContainer, Button } from "@svecodocs/kit";
const location = useGeolocation();
</script>

<DemoContainer class="flex flex-col gap-1">
<pre>Coords: {JSON.stringify(location.coords, null, 2)}</pre>
<pre>Located at: {location.locatedAt}</pre>
<pre>Error: {JSON.stringify(location.error, null, 2)}</pre>
<pre>Is Supported: {location.isSupported}</pre>
<div class="mt-4 flex items-center gap-2">
<Button size="sm" onclick={location.pause} disabled={location.isPaused}>Pause</Button>
<Button size="sm" onclick={location.resume} disabled={!location.isPaused}>Resume</Button>
</div>
</DemoContainer>

0 comments on commit b788207

Please sign in to comment.