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

Freegle / iznik-nuxt3 / ab05f487-76a6-4c8f-9bbc-5cf6ea8520f8

11 Dec 2025 11:02PM UTC coverage: 43.916% (+0.8%) from 43.156%
ab05f487-76a6-4c8f-9bbc-5cf6ea8520f8

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

2884 of 7194 branches covered (40.09%)

Branch coverage included in aggregate %.

6 of 12 new or added lines in 2 files covered. (50.0%)

213 existing lines in 9 files now uncovered.

3432 of 7188 relevant lines covered (47.75%)

16.7 hits per line

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

50.0
/components/MyPostsPostsList.vue
1
<template>
2
  <div class="my-posts-list">
38✔
3
    <!-- Active posts count header -->
4
    <div v-if="!loading && activePosts.length > 0" class="active-posts-header">
38✔
5
      <v-icon icon="gift" class="me-2" />
6
      {{ formattedActivePostsCount }}
7
    </div>
8

9
    <!-- Old posts toggle button -->
10
    <div v-if="!loading && oldPosts.length > 0" class="old-posts-toggle">
11
      <button class="toggle-btn" @click="toggleShowOldPosts">
12
        <v-icon :icon="showOldPosts ? 'eye-slash' : 'eye'" class="me-2" />
×
13
        {{ showOldPosts ? 'Hide' : 'Show' }} {{ formattedOldPostsCount }}
×
14
      </button>
15
    </div>
16

17
    <!-- Upcoming collections -->
18
    <div v-if="!loading && upcomingTrysts.length > 0" class="collections-card">
19
      <h3 class="collections-title">
20
        <v-icon icon="calendar-check" class="me-2" />Your upcoming collections
×
21
      </h3>
22
      <div
23
        v-for="tryst in upcomingTrysts"
×
24
        :key="'tryst-' + tryst.id"
25
        class="collection-item"
26
      >
27
        <v-icon icon="calendar-alt" class="collection-icon" />
28
        <div class="collection-info">
29
          <span class="collection-date">{{ tryst.trystdate }}</span>
30
          <span class="collection-details">
31
            {{ tryst.name }} collecting <em>{{ tryst.subject }}</em>
32
          </span>
33
        </div>
34
      </div>
35
    </div>
36

37
    <!-- Loading state -->
38
    <div v-if="loading" class="loading-state">
38✔
39
      <b-img lazy src="/loader.gif" alt="Loading..." width="60px" />
24✔
40
    </div>
41

42
    <!-- Posts list -->
43
    <div v-else-if="visiblePosts.length > 0" class="posts-container">
44
      <div v-for="post in visiblePosts" :key="'post-' + post.id">
45
        <Suspense>
46
          <MyMessage
47
            :id="post.id"
48
            :show-old="showOldPosts"
49
            :expand="defaultExpanded"
50
          />
51
          <template #fallback>
52
            <div class="loading-placeholder">
53
              <b-img lazy src="/loader.gif" alt="Loading" width="80px" />
54
            </div>
55
          </template>
56
        </Suspense>
57
      </div>
58
      <InfiniteLoading
32✔
59
        :distance="scrollboxHeight"
60
        @infinite="(event) => emit('load-more', event)"
61
      />
62
    </div>
63

64
    <!-- Empty state -->
65
    <div v-else class="empty-state">
19✔
66
      <v-icon icon="folder-open" class="empty-icon" />
67
      <p class="empty-text">You have no active posts.</p>
14!
68
      <div class="empty-actions">
69
        <template v-if="props.type === 'Offer'">
70
          <nuxt-link to="/give" class="mobile-btn mobile-btn--give">
71
            <v-icon icon="gift" class="me-2" />Give stuff
72
          </nuxt-link>
14!
73
        </template>
74
        <template v-else-if="props.type === 'Wanted'">
×
75
          <nuxt-link to="/find" class="mobile-btn mobile-btn--find">
76
            <v-icon icon="search" class="me-2" />Find stuff
77
          </nuxt-link>
78
        </template>
79
        <template v-else>
80
          <nuxt-link to="/give" class="mobile-btn mobile-btn--give">
81
            <v-icon icon="gift" class="me-2" />Give stuff
82
          </nuxt-link>
16✔
83
          <nuxt-link to="/find" class="mobile-btn mobile-btn--find">
84
            <v-icon icon="search" class="me-2" />Find stuff
85
          </nuxt-link>
86
        </template>
87
      </div>
88
    </div>
89
  </div>
90
</template>
91
<script setup>
92
import pluralize from 'pluralize'
93
import dayjs from 'dayjs'
94
import MyMessage from '~/components/MyMessage.vue'
95
import InfiniteLoading from '~/components/InfiniteLoading.vue'
96
import { useMessageStore } from '~/stores/message'
97
import { useUserStore } from '~/stores/user'
98
import { useTrystStore } from '~/stores/tryst'
8✔
99

8✔
100
const messageStore = useMessageStore()
8✔
101
const userStore = useUserStore()
102
const trystStore = useTrystStore()
8✔
103

104
const props = defineProps({
105
  posts: { type: Array, required: true },
106
  loading: { type: Boolean, required: true },
107
  defaultExpanded: { type: Boolean, required: true },
108
  show: { type: Number, required: true },
109
})
8✔
110

111
const emit = defineEmits(['load-more'])
8✔
112

113
const scrollboxHeight = ref(1000)
8✔
UNCOV
114

×
UNCOV
115
const showOldPosts = ref(false)
×
116
function toggleShowOldPosts() {
117
  showOldPosts.value = !showOldPosts.value
118
}
119

8✔
120
// Posts are now passed directly as props
22!
121
const posts = computed(() => {
122
  return props.posts || []
123
})
124

8✔
125
// old posts are those with an outcome
22✔
126
const oldPosts = computed(() => {
127
  return posts.value.filter((post) => post.hasoutcome)
128
})
8✔
UNCOV
129

×
130
const formattedOldPostsCount = computed(() => {
131
  return pluralize(`old post`, oldPosts.value.length, true)
132
})
8✔
133

11✔
134
const formattedActivePostsCount = computed(() => {
135
  return pluralize(`active post`, activePosts.value.length, true)
136
})
8✔
137

22✔
138
const activePosts = computed(() => {
139
  return posts.value.filter((post) => !post.hasoutcome)
140
})
8✔
141

142
watch(activePosts, (newVal) => {
143
  // For messages which are promised and not successful, we need to trigger a fetch.  This is so
14✔
144
  // that we can correctly show the upcoming collections.
8!
145
  newVal.forEach((post) => {
13!
146
    if (
147
      post.type === 'Offer' &&
148
      post.promised &&
149
      !post.hasoutcome &&
UNCOV
150
      !messageStore.byId(post.id)
×
151
    ) {
152
      messageStore.fetch(post.id)
153
    }
154
  })
155
})
8✔
156

22!
157
const visiblePosts = computed(() => {
22!
158
  let visiblePostList = showOldPosts.value ? posts.value : activePosts.value
159
  visiblePostList = visiblePostList || []
22✔
160

161
  const result = visiblePostList
UNCOV
162
    .toSorted((a, b) => {
×
UNCOV
163
      // promised items first, then by most recently
×
164
      if (!showOldPosts.value && a.promised && !b.promised) {
×
165
        return -1
×
166
      } else if (!showOldPosts.value && b.promised && !a.promised) {
167
        return 1
×
168
      } else {
169
        return new Date(b.arrival).getTime() - new Date(a.arrival).getTime()
170
      }
171
    })
172
    .slice(0, props.show)
22✔
173

174
  return result
175
})
8✔
176

27✔
177
const upcomingTrysts = computed(() => {
178
  const ret = []
27✔
179

16✔
180
  activePosts.value.forEach((post) => {
16!
UNCOV
181
    const message = messageStore.byId(post.id)
×
UNCOV
182
    if (post.type === 'Offer' && message?.promises?.length) {
×
183
      message.promises.forEach((p) => {
184
        const user = userStore?.byId(p.userid)
×
UNCOV
185

×
186
        if (user) {
187
          const tryst = trystStore?.getByUser(p.userid)
UNCOV
188

×
189
          // If tryst.arrangedfor is in the future or within the last hour
×
190
          if (
191
            tryst &&
192
            new Date(tryst.arrangedfor).getTime() >
UNCOV
193
              new Date().getTime() - 60 * 60 * 1000
×
194
          ) {
195
            const date = tryst
196
              ? dayjs(tryst.arrangedfor).format('dddd Do HH:mm a')
UNCOV
197
              : null
×
198

199
            ret.push({
200
              id: p.userid,
201
              name: user.displayname,
202
              tryst,
203
              trystdate: date,
204
              subject: message.subject,
205
            })
206
          }
207
        }
208
      })
209
    }
210
  })
27✔
UNCOV
211

×
212
  return ret.toSorted((a, b) => {
213
    return (
214
      new Date(a.tryst.arrangedfor).getTime() -
215
      new Date(b.tryst.arrangedfor).getTime()
216
    )
217
  })
218
})
219
</script>
220
<style scoped lang="scss">
221
@import 'assets/css/_color-vars.scss';
222

223
.my-posts-list {
224
  padding: 0;
225
}
226

227
.loading-state {
228
  display: flex;
229
  justify-content: center;
230
  align-items: center;
231
  min-height: 300px;
232
  padding: 40px;
233
  background: white;
234
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
235
  opacity: 0;
236
  animation: fadeIn 0.2s ease-in forwards;
237
  animation-delay: 0.3s;
238
}
239

240
@keyframes fadeIn {
241
  to {
242
    opacity: 1;
243
  }
244
}
245

246
.active-posts-header {
247
  display: flex;
248
  align-items: center;
249
  justify-content: center;
250
  width: 100%;
251
  padding: 10px 16px;
252
  margin-bottom: 12px;
253
  background: white;
254
  border: 1px solid $color-gray--light;
255
  color: $colour-success;
256
  font-weight: 500;
257
  font-size: 0.9rem;
258
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
259
}
260

261
.old-posts-toggle {
262
  margin-bottom: 12px;
263
}
264

265
.toggle-btn {
266
  display: flex;
267
  align-items: center;
268
  justify-content: center;
269
  width: 100%;
270
  padding: 10px 16px;
271
  background: white;
272
  border: 1px solid $color-gray--light;
273
  color: $color-gray--dark;
274
  font-weight: 500;
275
  font-size: 0.9rem;
276
  cursor: pointer;
277
  transition: all 0.2s;
278
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
279

280
  &:hover {
281
    background: $color-gray--lighter;
282
    border-color: $color-gray--base;
283
  }
284
}
285

286
.collections-card {
287
  background: white;
288
  border-radius: 12px;
289
  padding: 16px;
290
  margin-bottom: 16px;
291
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
292
  border-left: 4px solid $color-blue--bright;
293
}
294

295
.collections-title {
296
  display: flex;
297
  align-items: center;
298
  font-size: 1rem;
299
  font-weight: 600;
300
  color: $color-gray--darker;
301
  margin: 0 0 12px 0;
302
}
303

304
.collection-item {
305
  display: flex;
306
  align-items: flex-start;
307
  gap: 12px;
308
  padding: 10px 0;
309
  border-bottom: 1px solid $color-gray--lighter;
310

311
  &:last-child {
312
    border-bottom: none;
313
    padding-bottom: 0;
314
  }
315
}
316

317
.collection-icon {
318
  color: $color-blue--bright;
319
  font-size: 1rem;
320
  margin-top: 2px;
321
}
322

323
.collection-info {
324
  flex: 1;
325
}
326

327
.collection-date {
328
  display: block;
329
  font-weight: 600;
330
  color: $color-black;
331
  margin-bottom: 2px;
332
}
333

334
.collection-details {
335
  font-size: 0.9rem;
336
  color: $color-gray--dark;
337
}
338

339
.posts-container {
340
  display: flex;
341
  flex-direction: column;
342
  gap: 0;
343
}
344

345
.loading-placeholder {
346
  background: white;
347
  margin-bottom: 12px;
348
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
349
}
350

351
.loading-placeholder::before {
352
  content: '';
353
  display: block;
354
  width: 100%;
355
  padding-bottom: 50%;
356
  background: $color-gray--light;
357
}
358

359
.loading-more {
360
  display: flex;
361
  justify-content: center;
362
  padding: 20px;
363
}
364

365
.empty-state {
366
  display: flex;
367
  flex-direction: column;
368
  align-items: center;
369
  justify-content: center;
370
  padding: 48px 24px;
371
  background: white;
372
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
373
}
374

375
.empty-icon {
376
  font-size: 3rem;
377
  color: $color-gray--base;
378
  margin-bottom: 16px;
379
}
380

381
.empty-text {
382
  font-size: 1.1rem;
383
  color: $color-gray--dark;
384
  margin-bottom: 20px;
385
}
386

387
.empty-actions {
388
  display: flex;
389
  gap: 12px;
390
  flex-wrap: wrap;
391
  justify-content: center;
392
}
393

394
.mobile-btn {
395
  display: flex;
396
  align-items: center;
397
  justify-content: center;
398
  padding: 0.6rem 1.5rem;
399
  font-size: 0.9rem;
400
  font-weight: 600;
401
  text-decoration: none;
402
  transition: transform 0.1s;
403

404
  &:active {
405
    transform: scale(0.98);
406
  }
407

408
  &--give {
409
    background: $colour-success;
410
    color: $color-white;
411

412
    &:hover {
413
      background: darken($colour-success, 5%);
414
      color: $color-white;
415
    }
416
  }
417

418
  &--find {
419
    background: $colour-secondary;
420
    color: $color-white;
421

422
    &:hover {
423
      background: darken($colour-secondary, 5%);
424
      color: $color-white;
425
    }
426
  }
427
}
428

429
.minheight {
430
  min-height: 200px;
431
}
432
</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