feat(hunt): track walking distance via GPS, show live locations on map
Docker Image CI / test (push) Failing after 41s
Docker Image CI / deploy (push) Skipped
Tests / unit (push) Failing after 36s
Tests / integration (push) Successful in 1m34s

Add foreground-only GPS distance tracking for hunts
This commit is contained in:
2026-09-27 20:55:58 +02:00
parent a0fc836c05
commit 61cc516f47
43 changed files with 2313 additions and 24 deletions
@@ -74,6 +74,14 @@ const answer = ref(data.value?.answer?.text);
const final = ref(data.value?.answer?.final || false);
const privateImage = ref(data.value?.answer?.picture?.private ?? false);
const trackableHunt = data.value?.hunt ?? null;
const geo = trackableHunt ? useGeoTracking(trackableHunt) : null;
// Submit-location slice: independent of the tracking watch loop. The checkbox
// is offered only when the user consented and the hunt is tracking-eligible.
const sendLocation = ref(unref(geo?.consented?.value ?? false));
const locationNotice = ref('');
const privateChanged = computed(
() => (data.value?.answer?.picture?.private ?? false) !== privateImage.value
);
@@ -105,6 +113,7 @@ useEventListener(window, 'beforeunload', (ev) => {
async function submit() {
uploading.value = true;
submitError.value = '';
locationNotice.value = '';
uploadProgress.value = 0;
const body = new FormData();
@@ -127,6 +136,16 @@ async function submit() {
compressing.value = false;
body.set('image', compressedImage);
}
if (sendLocation.value && geo) {
try {
const pos = await geo.getPosition();
body.set('lat', String(pos.lat));
body.set('lon', String(pos.lon));
} catch {
// Non-blocking: submit without the location and tell the user.
locationNotice.value = t('page.quest.location_unavailable');
}
}
const xhr = new XMLHttpRequest();
try {
@@ -149,6 +168,7 @@ async function submit() {
answer.value = data.value?.answer?.text;
privateImage.value = data.value?.answer?.picture?.private ?? false;
newImage.value = undefined;
sendLocation.value = false;
} catch (e) {
if (xhr.status === 418) {
submitError.value = t('page.quest.already_finalized');
@@ -353,7 +373,20 @@ async function submit() {
:label="t('page.quest.finalize')"
:messages="t('page.quest.finalize_explain')"
/>
<VaCheckbox
v-if="geo?.gate"
v-model="sendLocation"
:disabled="uploading || data.answer?.final"
:label="t('page.quest.send_location')"
:messages="t('page.quest.send_location_explain')"
/>
<VaAlert
v-if="locationNotice.length > 0"
color="warning"
class="w-full text-center"
>{{ locationNotice }}</VaAlert
>
<VaAlert
v-if="submitError.length > 0"
color="danger"
@@ -367,7 +400,8 @@ async function submit() {
newImage ||
data.answer?.text !== answer ||
data.answer?.final !== final ||
privateChanged
privateChanged ||
sendLocation
) || uploading
"
:loading="uploading"