• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Freegle / iznik-nuxt3 / 99e3f760-c611-4bfd-88c0-b43b43faaba6

05 Dec 2025 11:02PM UTC coverage: 42.545% (+0.4%) from 42.1%
99e3f760-c611-4bfd-88c0-b43b43faaba6

push

circleci

actions-user
Auto-merge production to app-ci-fd (daily scheduled)

Automated merge from production branch after successful tests.

🤖 Automated by GitHub Actions

1890 of 4798 branches covered (39.39%)

Branch coverage included in aggregate %.

1 of 3 new or added lines in 1 file covered. (33.33%)

305 existing lines in 20 files now uncovered.

2376 of 5229 relevant lines covered (45.44%)

31.51 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

28.95
/components/PostCode.vue
1
<template>
2
  <div class="d-flex align-items-center">
3
    <div class="d-flex flex-column">
4
      <label v-if="label" :for="id">{{ label }}</label>
8!
5
      <div class="d-flex align-items-center">
6
        <div
7
          v-b-tooltip="{
8
            title: 'Keep typing your full postcode...',
9
            triggers: [],
10
            shown: wip && (!results || results?.length > 1),
11
            placement: 'top',
12
            delay: { show: 3000 },
13
          }"
14
          class="postcode-input-wrapper"
15
        >
16
          <AutoComplete
17
            :id="id"
18
            ref="autocomplete"
19
            :init-value="wip"
20
            restrict
21
            :url="source"
22
            :custom-params="{ pconly: pconly }"
23
            anchor="name"
24
            label=""
8!
25
            :placeholder="pconly ? 'Type postcode' : 'Type location'"
26
            :classes="{
27
              input: 'form-control form-control-' + size + ' text-center pcinp',
28
              list: 'postcodelist',
29
              listentry: 'w-100',
30
              listentrylist: 'listentry',
31
            }"
32
            class="mr-1"
33
            :min="3"
34
            :debounce="200"
35
            :process="process"
36
            :on-select="select"
37
            :size="10"
38
            :variant="variant"
39
            not-found-message="Not a valid postcode."
40
            @invalid="invalid"
41
          />
42
          <v-icon v-if="isValid" icon="check" class="validation-tick" />
43
        </div>
8!
44
        <b-popover
45
          v-if="showLocated"
46
          content="Your device thinks you're here. If it's wrong, please change it."
47
          :target="id"
48
          placement="top"
49
          variant="primary"
50
          :show="true"
51
          :skidding="-50"
16✔
52
        />
53
        <div v-if="find && !wip">
54
          <SpinButton
55
            style="line-height: 1.7em"
56
            variant="secondary"
57
            :flex="false"
58
            button-title="Find my device's location instead of typing a postcode"
59
            done-icon=""
60
            :icon-name="
61
              locationFailed ? 'exclamation-triangle' : 'map-marker-alt'
62
            "
63
            :size="size"
64
            @handle="findLoc"
65
          />
66
        </div>
67
      </div>
68
    </div>
69
  </div>
70
</template>
71
<script setup>
72
import SpinButton from './SpinButton'
73
import { uid } from '~/composables/useId'
74
import { useAuthStore } from '~/stores/auth'
75
import { useLocationStore } from '~/stores/location'
76
import {
77
  ref,
78
  computed,
79
  onMounted,
80
  onBeforeUnmount,
81
  useRuntimeConfig,
82
} from '#imports'
83
import { useComposeStore } from '~/stores/compose'
84
import AutoComplete from '~/components/AutoComplete'
8✔
85

86
const props = defineProps({
87
  value: {
88
    type: String,
89
    required: false,
90
    default: null,
91
  },
92
  label: {
93
    type: String,
94
    required: false,
95
    default: null,
96
  },
97
  focus: {
98
    type: Boolean,
99
    required: false,
100
    default: false,
101
  },
102
  find: {
103
    type: Boolean,
104
    required: false,
105
    default: true,
106
  },
107
  size: {
108
    type: String,
109
    required: false,
110
    default: 'lg',
111
  },
112
  pconly: {
113
    type: Boolean,
114
    required: false,
115
    default: true,
116
  },
117
  noStore: {
118
    type: Boolean,
119
    required: false,
120
    default: true,
121
  },
122
  variant: {
123
    type: String,
124
    required: false,
125
    default: null,
126
  },
127
})
8✔
128

129
const emit = defineEmits(['selected', 'cleared'])
8✔
130

8✔
131
const composeStore = useComposeStore()
8✔
132
const authStore = useAuthStore()
8✔
133
const locationStore = useLocationStore()
134
const runtimeConfig = useRuntimeConfig()
8✔
135

8✔
136
const wip = ref(props.value)
8✔
137
const results = ref([])
8✔
138
const locationFailed = ref(false)
8✔
139
const showLocated = ref(false)
8✔
140
const callbackToCall = ref(null)
141
const autocomplete = ref(null)
142
const isValid = ref(false)
8✔
143

144
// Unique id
8✔
145
const id = uid('postcode')
146

8!
147
const me = authStore.user
UNCOV
148

×
149
if (props.pconly && wip.value === null && me?.settings?.mylocation?.name) {
150
  // If we are logged in then we may have a known location to use as the default.
151
  wip.value = me?.settings?.mylocation?.name
8!
152
}
153

×
154
if (props.pconly && !wip.value && !props.noStore) {
155
  // We might have one we are composing.
×
156
  const pc = composeStore.postcode
×
157

158
  if (pc?.name) {
159
    wip.value = pc.name
160
  }
8✔
161
}
8✔
162

163
const source = computed(() => {
164
  return runtimeConfig.public.APIv2 + '/location/typeahead'
×
165
})
166

×
167
function invalid() {
×
168
  // Parent might want to know that we don't have a valid postcode any more.
×
169
  emit('cleared')
170
  wip.value = null
171
  results.value = []
×
172
  isValid.value = false
×
173
}
174

×
175
function keydown(e) {
176
  if (e.which === 8) {
177
    // Backspace means we no longer have a full postcode.
×
178
    invalid()
179
  } else {
180
    // Hide the tooltip in case it's showing from a use of the find button.
UNCOV
181
    showLocated.value = false
×
UNCOV
182
  }
×
UNCOV
183
}
×
184

UNCOV
185
function process(processResults) {
×
UNCOV
186
  const names = []
×
UNCOV
187
  const ret = []
×
188

UNCOV
189
  if (processResults) {
×
UNCOV
190
    for (let i = 0; i < processResults.length && names.length < 5; i++) {
×
UNCOV
191
      const loc = processResults[i]
×
192

193
      if (!names.includes(loc.name)) {
194
        names.push(loc.name)
195
        ret.push(loc)
UNCOV
196
      }
×
UNCOV
197
    }
×
198
  }
199

UNCOV
200
  results.value = ret
×
UNCOV
201
  return ret
×
UNCOV
202
}
×
UNCOV
203

×
204
async function select(pc) {
UNCOV
205
  console.log('Select', pc)
×
206
  if (pc) {
UNCOV
207
    if (pc.name && !pc.id) {
×
UNCOV
208
      // Find the location this corresponds to.
×
209
      const locs = await locationStore.typeahead(pc.name)
210

UNCOV
211
      if (locs?.length) {
×
212
        pc = locs[0]
213
      }
×
214
    }
215
    emit('selected', pc)
UNCOV
216
    isValid.value = true
×
217
  } else {
218
    emit('cleared')
219
    isValid.value = false
×
220
  }
×
221

222
  locationFailed.value = false
×
223
}
×
224

×
225
function findLoc(callback) {
226
  callbackToCall.value = callback
227

228
  try {
×
229
    if (
230
      navigator &&
×
231
      navigator.geolocation &&
232
      navigator.geolocation.getCurrentPosition
233
    ) {
234
      navigator.geolocation.getCurrentPosition(
235
        async (position) => {
×
236
          const res = await locationStore.fetchByLatLng(
237
            position.coords.latitude,
×
238
            position.coords.longitude
×
239
          )
240

241
          if ((res.lat || res.lng) && autocomplete.value) {
×
242
            // Got it - put it in the autocomplete input, and indicate that we've selected it.
×
243
            autocomplete.value.setValue(res.name)
244
            await select(res)
×
245

246
            // Show the user we've done this, and make them think.
247
            showLocated.value = true
248
            setTimeout(() => (showLocated.value = false), 10000)
×
249
          } else {
×
250
            locationFailed.value = true
251
          }
252
        },
253
        (e) => {
×
254
          console.error('Find location failed with', e)
×
255
          locationFailed.value = true
256
        }
257
      )
×
258
    } else {
×
259
      console.log('Navigation not supported.  ')
260
      locationFailed.value = true
×
261
    }
×
262
  } catch (e) {
263
    console.error('Find location failed with', e)
264
    locationFailed.value = true
265
  } finally {
8✔
266
    callbackToCall.value = null
8!
267
    callback()
8!
268
  }
269
}
×
270

271
onMounted(() => {
272
  if (autocomplete.value) {
273
    if (props.focus) {
8✔
274
      // Focus on postcode to grab their attention.
8✔
275
      autocomplete.value.$refs.input.focus()
276
    }
277

278
    // We need some fettling of the input keystrokes.
279
    const input = autocomplete.value.$refs.input
8!
UNCOV
280
    input.addEventListener('keydown', keydown, false)
×
281
  } else {
282
    // Not quite sure how this happens, but it does.
283
  }
284

285
  if (wip.value) {
286
    select({
8✔
UNCOV
287
      name: wip.value,
×
288
    })
×
289
  }
290
})
291

292
onBeforeUnmount(() => {
293
  if (callbackToCall.value) {
294
    callbackToCall.value()
295
  }
296
})
297
</script>
298
<style scoped lang="scss">
299
@import 'assets/css/_color-vars.scss';
300

301
.postcode-input-wrapper {
302
  position: relative;
303
  display: inline-flex;
304
  align-items: center;
305
}
306

307
:deep(.listentry) {
308
  width: 100%;
309
  right: 0 !important;
310
  text-align: center;
311
  border-color: $color-blue--light;
312
  outline: 0;
313
  box-shadow: 0 1px 0 0.2rem rgba(0, 123, 255, 0.25);
314
}
315

316
:deep(.popover) {
317
  background-color: black;
318
}
319

320
.validation-tick {
321
  position: absolute;
322
  right: 2.5rem;
323
  top: 50%;
324
  transform: translateY(-50%);
325
  color: $color-green-background;
326
  font-size: 1.25rem;
327
  pointer-events: none;
328
  z-index: 10;
329
}
330
</style>
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc