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

Freegle / iznik-nuxt3 / 9b74b3d3-ab16-4543-bae8-afb302315870

06 Dec 2025 11:02PM UTC coverage: 43.206% (+0.7%) from 42.545%
9b74b3d3-ab16-4543-bae8-afb302315870

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

2781 of 7090 branches covered (39.22%)

Branch coverage included in aggregate %.

28 of 67 new or added lines in 8 files covered. (41.79%)

517 existing lines in 28 files now uncovered.

3334 of 7063 relevant lines covered (47.2%)

14.04 hits per line

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

60.76
/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>
17!
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=""
25
            :placeholder="pconly ? 'Type postcode' : 'Type location'"
17!
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" />
17✔
43
        </div>
44
        <b-popover
45
          v-if="showLocated"
17!
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"
52
        />
53
        <div v-if="find && !wip">
34✔
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'
85

86
const props = defineProps({
12✔
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
})
128

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

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

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

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

147
const me = authStore.user
12✔
148

149
if (props.pconly && wip.value === null && me?.settings?.mylocation?.name) {
12✔
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
1!
152
}
153

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

158
  if (pc?.name) {
2!
UNCOV
159
    wip.value = pc.name
×
160
  }
161
}
162

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

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

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

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

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

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

200
  results.value = ret
3✔
201
  return ret
3✔
202
}
203

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

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

222
  locationFailed.value = false
4✔
223
}
224

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

228
  try {
×
UNCOV
229
    if (
×
230
      navigator &&
×
231
      navigator.geolocation &&
232
      navigator.geolocation.getCurrentPosition
233
    ) {
UNCOV
234
      navigator.geolocation.getCurrentPosition(
×
235
        async (position) => {
UNCOV
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.
UNCOV
243
            autocomplete.value.setValue(res.name)
×
244
            await select(res)
×
245

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

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

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

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

292
onBeforeUnmount(() => {
12✔
293
  if (callbackToCall.value) {
3!
UNCOV
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