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

Freegle / iznik-nuxt3 / 87afe2cc-83c0-41d0-b68b-b0c6a3d6fadb

19 Dec 2025 11:02PM UTC coverage: 42.889% (+0.02%) from 42.868%
87afe2cc-83c0-41d0-b68b-b0c6a3d6fadb

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

2845 of 7272 branches covered (39.12%)

Branch coverage included in aggregate %.

18 of 45 new or added lines in 6 files covered. (40.0%)

203 existing lines in 8 files now uncovered.

3391 of 7268 relevant lines covered (46.66%)

13.93 hits per line

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

0.0
/components/JobOne.vue
1
<template>
2
  <div v-if="job" class="job-item" @click="clicked">
3
    <ExternalLink :href="job.url" class="job-link">
4
      <div v-if="summary" class="job-summary">
5
        <div class="job-icon" :style="iconStyle">
6
          <img
7
            v-if="imageUrl"
×
8
            v-show="imageLoaded"
9
            :src="imageUrl"
10
            alt=""
11
            class="job-ai-image"
12
            loading="lazy"
13
            @load="imageLoaded = true"
×
14
            @error="imageLoaded = false"
×
15
          />
16
          <v-icon v-show="!imageUrl || !imageLoaded" icon="briefcase" />
×
17
        </div>
18
        <div class="job-content">
19
          <div class="job-title-row">
20
            <span class="job-title">{{ title }}</span>
21
          </div>
22
          <span v-if="job.location" class="job-location">
×
23
            <v-icon icon="map-marker-alt" class="location-icon" />
24
            {{ location }}
25
          </span>
26
        </div>
27
        <v-icon icon="chevron-right" class="job-chevron" />
28
      </div>
29
      <div
30
        v-else
31
        class="job-card"
32
        :class="{ 'job-card--highlight': highlight }"
33
      >
34
        <div class="job-card-image" :style="cardImageStyle">
35
          <img
36
            v-if="imageUrl"
×
37
            v-show="imageLoaded"
38
            :src="imageUrl"
39
            alt=""
40
            class="job-card-img"
41
            loading="lazy"
42
            @load="imageLoaded = true"
×
43
            @error="imageLoaded = false"
×
44
          />
45
          <div v-show="!imageUrl || !imageLoaded" class="job-card-placeholder">
×
46
            <v-icon icon="briefcase" />
47
          </div>
48
        </div>
49
        <div class="job-card-body">
50
          <h4 class="job-card-title">{{ title }}</h4>
51
          <span v-if="job.location" class="job-location">
×
52
            <v-icon icon="map-marker-alt" class="location-icon" />
53
            {{ location }}
54
          </span>
55
        </div>
56
      </div>
57
    </ExternalLink>
58
  </div>
59
</template>
60
<script setup>
61
import { computed, ref, onMounted } from 'vue'
62
import { useRouter } from 'vue-router'
63
import { useJobStore } from '~/stores/job'
64
import { action } from '~/composables/useClientLog'
65
import ExternalLink from '~/components/ExternalLink'
66
import { JOB_ICON_COLOURS } from '~/constants'
UNCOV
67

×
68
const imageLoaded = ref(false)
UNCOV
69

×
70
const props = defineProps({
71
  id: {
72
    type: Number,
73
    required: true,
74
  },
75
  summary: {
76
    type: Boolean,
77
    required: false,
78
    default: false,
79
  },
80
  highlight: {
81
    type: Boolean,
82
    required: false,
83
    default: false,
84
  },
85
  showBody: {
86
    type: Boolean,
87
    required: false,
88
    default: true,
89
  },
90
  className: {
91
    type: String,
92
    required: false,
93
    default: '',
94
  },
95
  bgColour: {
96
    type: String,
97
    required: false,
98
    default: 'dark green',
99
  },
100
  position: {
NEW
101
    type: Number,
×
NEW
102
    required: false,
×
103
    default: null,
NEW
104
  },
×
NEW
105
  listLength: {
×
106
    type: Number,
107
    required: false,
NEW
108
    default: null,
×
NEW
109
  },
×
NEW
110
  context: {
×
111
    type: String,
112
    required: false,
NEW
113
    default: null,
×
114
  },
115
})
116

117
const router = useRouter()
×
118
const jobStore = useJobStore()
×
119

120
const job = computed(() => {
121
  return jobStore?.byId(props.id)
×
UNCOV
122
})
×
123

×
124
const title = computed(() => {
125
  if (!job.value?.title) {
126
    return ''
UNCOV
127
  }
×
128

129
  return filterNonsense(job.value.title)
×
130
})
131

132
// Use server-provided image URL if available
133
const imageUrl = computed(() => {
×
134
  return job.value?.image || null
×
UNCOV
135
})
×
136

137
const location = computed(() => {
138
  if (
×
UNCOV
139
    job.value &&
×
UNCOV
140
    job.value.location &&
×
141
    job.value.location.indexOf(', ') === 0
142
  ) {
143
    return job.value.location.substring(2)
×
UNCOV
144
  } else {
×
145
    return job.value.location
146
  }
147
})
148

149
const iconStyle = computed(() => {
×
150
  const bg = JOB_ICON_COLOURS[props.bgColour] || JOB_ICON_COLOURS['dark green']
×
151
  return { background: bg, color: 'rgba(255, 255, 255, 0.6)' }
152
})
153

154
const cardImageStyle = computed(() => {
×
155
  const bg = JOB_ICON_COLOURS[props.bgColour] || JOB_ICON_COLOURS['dark green']
×
156
  return { background: bg }
157
})
158

159
// Log ad impression when job is rendered (client-side only).
160
onMounted(() => {
161
  if (job.value) {
162
    action('ad_impression', {
163
      job_id: job.value.id,
164
      job_reference: job.value.job_reference,
165
      job_category: job.value.category,
166
      cpc: job.value.cpc,
167
      position: props.position,
168
      list_length: props.listLength,
169
      context: props.context,
170
    })
171
  }
172
})
173

174
function clicked() {
175
  // Log to server for revenue tracking.
176
  jobStore.log({
177
    id: job.value.id,
178
  })
179

180
  // Log click to client log for analytics.
181
  action('ad_click', {
182
    job_id: job.value.id,
183
    job_reference: job.value.job_reference,
184
    job_category: job.value.category,
185
    cpc: job.value.cpc,
186
    position: props.position,
187
    list_length: props.listLength,
188
    context: props.context,
189
  })
190

191
  // Route to jobs page to encourage viewing of more jobs.
192
  if (router?.currentRoute?.value?.path !== '/jobs') {
193
    router.push('/jobs')
194
  }
195
}
196

197
function filterNonsense(val) {
198
  return val
199
    .replace(/\\n/g, '\n')
200
    .replace(/<br>/g, '\n')
201
    .replace(/£/g, '£')
202
    .trim()
203
    .normalize('NFD')
204
    .replace(/[\u0300-\u036F]/g, '')
205
}
206
</script>
207
<style scoped lang="scss">
208
@import 'bootstrap/scss/functions';
209
@import 'bootstrap/scss/variables';
210
@import 'bootstrap/scss/mixins/_breakpoints';
211

212
.job-item {
213
  margin-bottom: 0.5rem;
214
}
215

216
.job-link {
217
  text-decoration: none;
218
  color: inherit;
219

220
  &:hover {
221
    text-decoration: none;
222
  }
223
}
224

225
/* Summary mode - compact row */
226
.job-summary {
227
  display: flex;
228
  align-items: center;
229
  gap: 0.5rem;
230
  padding: 0.5rem;
231
  background: $white;
232
  border-bottom: 1px solid $gray-200;
233
  transition: background-color 0.15s ease;
234

235
  @include media-breakpoint-up(sm) {
236
    gap: 0.75rem;
237
    padding: 0.6rem 0.75rem;
238
  }
239

240
  &:hover {
241
    background: $gray-100;
242
  }
243

244
  &:active {
245
    background: $gray-200;
246
  }
247
}
248

249
.job-icon {
250
  display: flex;
251
  align-items: center;
252
  justify-content: center;
253
  width: 2.5rem;
254
  height: 2.5rem;
255
  background: $gray-100;
256
  color: $gray-500;
257
  flex-shrink: 0;
258
  font-size: 1rem;
259

260
  @include media-breakpoint-up(sm) {
261
    width: 3rem;
262
    height: 3rem;
263
    font-size: 1.25rem;
264
  }
265
}
266

267
.job-ai-image {
268
  width: 2.5rem;
269
  height: 2.5rem;
270
  object-fit: cover;
271

272
  @include media-breakpoint-up(sm) {
273
    width: 3rem;
274
    height: 3rem;
275
  }
276
}
277

278
.job-content {
279
  flex: 1;
280
  min-width: 0;
281
}
282

283
.job-title-row {
284
  display: flex;
285
  align-items: baseline;
286
  gap: 0.5rem;
287
}
288

289
.job-title {
290
  font-size: 0.75rem;
291
  font-weight: 600;
292
  color: $gray-800;
293
  display: -webkit-box;
294
  -webkit-line-clamp: 2;
295
  -webkit-box-orient: vertical;
296
  overflow: hidden;
297
  line-height: 1.3;
298
  min-height: calc(2 * 1.3 * 0.75rem);
299

300
  @include media-breakpoint-up(sm) {
301
    font-size: 0.85rem;
302
    min-height: calc(2 * 1.3 * 0.85rem);
303
  }
304

305
  @include media-breakpoint-up(md) {
306
    font-size: 0.95rem;
307
    min-height: calc(2 * 1.3 * 0.95rem);
308
  }
309
}
310

311
.job-location {
312
  display: flex;
313
  align-items: center;
314
  gap: 0.2rem;
315
  font-size: 0.65rem;
316
  color: $gray-600;
317
  margin-top: 0.1rem;
318

319
  @include media-breakpoint-up(sm) {
320
    font-size: 0.75rem;
321
    gap: 0.25rem;
322
  }
323

324
  .location-icon {
325
    font-size: 0.6rem;
326
    color: $gray-500;
327

328
    @include media-breakpoint-up(sm) {
329
      font-size: 0.7rem;
330
    }
331
  }
332
}
333

334
.job-chevron {
335
  display: none;
336
  color: $gray-400;
337
  flex-shrink: 0;
338

339
  @include media-breakpoint-up(sm) {
340
    display: block;
341
  }
342
}
343

344
/* Card mode - mosaic display */
345
.job-card {
346
  background: $white;
347
  border: 1px solid $gray-200;
348
  overflow: hidden;
349
  transition: box-shadow 0.15s ease, border-color 0.15s ease;
350
  display: flex;
351
  flex-direction: column;
352

353
  &:hover {
354
    border-color: #61ae24;
355
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
356
  }
357

358
  &--highlight {
359
    border-left: 3px solid #61ae24;
360
  }
361
}
362

363
.job-card-image {
364
  aspect-ratio: 1;
365
  display: flex;
366
  align-items: center;
367
  justify-content: center;
368
  overflow: hidden;
369
}
370

371
.job-card-img {
372
  width: 100%;
373
  height: 100%;
374
  object-fit: cover;
375
}
376

377
.job-card-placeholder {
378
  display: flex;
379
  align-items: center;
380
  justify-content: center;
381
  width: 100%;
382
  height: 100%;
383
  color: rgba(255, 255, 255, 0.5);
384
  font-size: 3rem;
385
}
386

387
.job-card-body {
388
  padding: 0.75rem;
389
  flex: 1;
390
  display: flex;
391
  flex-direction: column;
392
}
393

394
.job-card-title {
395
  font-size: 0.95rem;
396
  font-weight: 600;
397
  color: $gray-800;
398
  margin: 0 0 0.25rem 0;
399
  display: -webkit-box;
400
  -webkit-line-clamp: 2;
401
  -webkit-box-orient: vertical;
402
  overflow: hidden;
403
  line-height: 1.3;
404
}
405
</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