feat(hunt): track walking distance via GPS, show live locations on map
Add foreground-only GPS distance tracking for hunts
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
type TrackableHunt,
|
||||
useGeoTracking
|
||||
} from '~/composables/useGeoTracking';
|
||||
|
||||
const props = defineProps<{
|
||||
hunt: TrackableHunt;
|
||||
seedDistance?: number;
|
||||
}>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { formatDistance } = useFormatDistance();
|
||||
|
||||
const {
|
||||
distance,
|
||||
lastFix,
|
||||
paused,
|
||||
consented,
|
||||
eligible,
|
||||
gate,
|
||||
isSupported,
|
||||
start,
|
||||
stop,
|
||||
toggleConsent
|
||||
} = useGeoTracking(props.hunt, props.seedDistance ?? 0);
|
||||
|
||||
// Ticking clock so the "last fix … ago" label stays live while the card is open.
|
||||
const nowTs = ref(Date.now());
|
||||
let clock: ReturnType<typeof setInterval> | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
clock = setInterval(() => (nowTs.value = Date.now()), 1000);
|
||||
});
|
||||
|
||||
onScopeDispose(() => {
|
||||
if (clock !== undefined) clearInterval(clock);
|
||||
});
|
||||
|
||||
const lastFixAgo = computed(() => {
|
||||
if (!lastFix.value) return '';
|
||||
const seconds = Math.max(
|
||||
0,
|
||||
Math.round((nowTs.value - lastFix.value.getTime()) / 1000)
|
||||
);
|
||||
return new Intl.RelativeTimeFormat(locale.value || 'en', {
|
||||
numeric: 'auto'
|
||||
}).format(-seconds, 'second');
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VaCard
|
||||
v-if="eligible"
|
||||
stripe
|
||||
:stripe-color="gate && !paused ? 'success' : 'primary'"
|
||||
>
|
||||
<VaCardTitle>{{ t('page.hunt.tracking.title') }}</VaCardTitle>
|
||||
<VaCardContent>
|
||||
<div class="flex flex-col gap-3">
|
||||
<VaAlert v-if="!isSupported" color="warning">
|
||||
{{ t('page.hunt.tracking.unsupported') }}
|
||||
</VaAlert>
|
||||
<VaSwitch
|
||||
:model-value="consented"
|
||||
:disabled="!isSupported"
|
||||
:label="t('page.hunt.tracking.consent')"
|
||||
@update:model-value="toggleConsent"
|
||||
/>
|
||||
<p class="text-sm opacity-70">
|
||||
{{ t('page.hunt.tracking.consent_explain') }}
|
||||
</p>
|
||||
<template v-if="consented">
|
||||
<div class="grid grid-cols-2 gap-2 text-lg md:grid-cols-3">
|
||||
<VaChip :color="paused ? 'secondary' : 'success'">
|
||||
{{
|
||||
t(
|
||||
paused
|
||||
? 'page.hunt.tracking.status_paused'
|
||||
: 'page.hunt.tracking.status_tracking'
|
||||
)
|
||||
}}
|
||||
</VaChip>
|
||||
<span class="font-bold">{{ formatDistance(distance) }}</span>
|
||||
<span
|
||||
v-if="lastFixAgo"
|
||||
class="col-span-2 text-sm opacity-70 md:col-span-1"
|
||||
>
|
||||
{{ t('page.hunt.tracking.last_fix', { ago: lastFixAgo }) }}
|
||||
</span>
|
||||
</div>
|
||||
<VaButton
|
||||
:disabled="!isSupported"
|
||||
:icon="paused ? 'play_arrow' : 'pause'"
|
||||
@click="paused ? start() : stop()"
|
||||
>
|
||||
{{
|
||||
t(
|
||||
paused
|
||||
? 'page.hunt.tracking.resume'
|
||||
: 'page.hunt.tracking.pause'
|
||||
)
|
||||
}}
|
||||
</VaButton>
|
||||
</template>
|
||||
</div>
|
||||
</VaCardContent>
|
||||
</VaCard>
|
||||
</template>
|
||||
Reference in New Issue
Block a user