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

Freegle / iznik-nuxt3 / e41dd74a-64a5-4c97-9f4a-dd1b3d8a6989

14 Jan 2026 11:02PM UTC coverage: 43.671% (-0.007%) from 43.678%
e41dd74a-64a5-4c97-9f4a-dd1b3d8a6989

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

3604 of 8352 branches covered (43.15%)

Branch coverage included in aggregate %.

5 of 13 new or added lines in 4 files covered. (38.46%)

117 existing lines in 7 files now uncovered.

1895 of 4240 relevant lines covered (44.69%)

85.08 hits per line

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

49.45
/components/MyPostsPostsList.vue
1
<template>
2
  <div class="my-posts-list">
234✔
3
    <!-- Active posts count header -->
4
    <div v-if="!loading && activePosts.length > 0" class="active-posts-header">
5
      <v-icon icon="gift" class="me-2" />
6
      {{ formattedActivePostsCount }}
590!
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 }}
590!
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">
234✔
39
      <b-img lazy src="/loader.gif" alt="Loading..." width="60px" />
40
    </div>
41

42
    <!-- Posts list -->
43
    <div v-else-if="visiblePosts.length > 0" class="posts-container">
122✔
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
59
        :distance="scrollboxHeight"
60
        @infinite="(event) => emit('load-more', event)"
120✔
61
      />
62
    </div>
63

64
    <!-- Empty state -->
65
    <div v-else class="empty-state">
66
      <v-icon icon="folder-open" class="empty-icon" />
67
      <p class="empty-text">You have no active posts.</p>
75✔
68
      <div class="empty-actions">
69
        <template v-if="props.type === 'Offer'">
47!
70
          <nuxt-link to="/give" class="mobile-btn mobile-btn--give">
71
            <v-icon icon="gift" class="me-2" />Give stuff
×
72
          </nuxt-link>
73
        </template>
74
        <template v-else-if="props.type === 'Wanted'">
47!
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
79✔
82
          </nuxt-link>
83
          <nuxt-link to="/find" class="mobile-btn mobile-btn--find">
84
            <v-icon icon="search" class="me-2" />Find stuff
79✔
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'
99

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

104
const props = defineProps({
105
  posts: { type: Array, required: true },
106
  postIds: { type: Array, required: false, default: () => [] },
107
  loading: { type: Boolean, required: true },
108
  defaultExpanded: { type: Boolean, required: true },
109
  show: { type: Number, required: true },
110
})
111

112
const emit = defineEmits(['load-more'])
113

114
const scrollboxHeight = ref(1000)
115

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

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

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

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

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

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

60✔
143
const postIds = computed(() => {
144
  return props.posts.map((post) => post.id)
145
})
73✔
146

147
watch(postIds, (newIds, oldIds) => {
49!
148
  // Fetch new messages when postIds change
149
  if (oldIds && newIds.length !== oldIds.length) {
150
    const newPostIds = newIds.filter((id) => !oldIds.includes(id))
151
    newPostIds.forEach((id) => {
152
      if (!messageStore.byId(id)) {
153
        messageStore.fetch(id)
154
      }
155
    })
156
  }
157
})
60✔
158

77!
159
watch(activePosts, (newVal) => {
77!
160
  // For messages which are promised and not successful, we need to trigger a fetch.  This is so
161
  // that we can correctly show the upcoming collections.
162
  newVal.forEach((post) => {
163
    if (
164
      post.type === 'Offer' &&
18!
165
      post.promised &&
166
      !post.hasoutcome &&
6!
167
      !messageStore.byId(post.id)
168
    ) {
169
      messageStore.fetch(post.id)
170
    }
171
  })
172
})
173

174
const visiblePosts = computed(() => {
175
  let visiblePostList = showOldPosts.value ? posts.value : activePosts.value
176
  visiblePostList = visiblePostList || []
177

178
  const result = visiblePostList
99✔
179
    .toSorted((a, b) => {
180
      // promised items first, then by most recently
99✔
181
      if (!showOldPosts.value && a.promised && !b.promised) {
76✔
182
        return -1
76!
183
      } else if (!showOldPosts.value && b.promised && !a.promised) {
UNCOV
184
        return 1
×
185
      } else {
UNCOV
186
        return new Date(b.arrival).getTime() - new Date(a.arrival).getTime()
×
UNCOV
187
      }
×
188
    })
189
    .slice(0, props.show)
UNCOV
190

×
191
  return result
×
192
})
193

194
const upcomingTrysts = computed(() => {
UNCOV
195
  const ret = []
×
196

197
  activePosts.value.forEach((post) => {
198
    const message = messageStore.byId(post.id)
UNCOV
199
    if (post.type === 'Offer' && message?.promises?.length) {
×
200
      message.promises.forEach((p) => {
201
        const user = userStore?.byId(p.userid)
202

203
        if (user) {
204
          const tryst = trystStore?.getByUser(p.userid)
205

206
          // If tryst.arrangedfor is in the future or within the last hour
207
          if (
208
            tryst &&
209
            new Date(tryst.arrangedfor).getTime() >
210
              new Date().getTime() - 60 * 60 * 1000
211
          ) {
212
            const date = tryst
213
              ? dayjs(tryst.arrangedfor).format('dddd Do HH:mm a')
214
              : null
215

216
            ret.push({
217
              id: p.userid,
218
              name: user.displayname,
219
              tryst,
220
              trystdate: date,
221
              subject: message.subject,
222
            })
223
          }
224
        }
225
      })
226
    }
227
  })
228

229
  return ret.toSorted((a, b) => {
230
    return (
231
      new Date(a.tryst.arrangedfor).getTime() -
232
      new Date(b.tryst.arrangedfor).getTime()
233
    )
234
  })
235
})
236
</script>
237
<style scoped lang="scss">
238
@import 'assets/css/_color-vars.scss';
239

240
.my-posts-list {
241
  padding: 0;
242
}
243

244
.loading-state {
245
  display: flex;
246
  justify-content: center;
247
  align-items: center;
248
  min-height: 300px;
249
  padding: 40px;
250
  background: white;
251
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
252
  opacity: 0;
253
  animation: fadeIn 0.2s ease-in forwards;
254
  animation-delay: 0.3s;
255
}
256

257
@keyframes fadeIn {
258
  to {
259
    opacity: 1;
260
  }
261
}
262

263
.active-posts-header {
264
  display: flex;
265
  align-items: center;
266
  justify-content: center;
267
  width: 100%;
268
  padding: 10px 16px;
269
  margin-bottom: 12px;
270
  background: white;
271
  border: 1px solid $color-gray--light;
272
  color: $colour-success;
273
  font-weight: 500;
274
  font-size: 0.9rem;
275
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
276
}
277

278
.old-posts-toggle {
279
  margin-bottom: 12px;
280
}
281

282
.toggle-btn {
283
  display: flex;
284
  align-items: center;
285
  justify-content: center;
286
  width: 100%;
287
  padding: 10px 16px;
288
  background: white;
289
  border: 1px solid $color-gray--light;
290
  color: $color-gray--dark;
291
  font-weight: 500;
292
  font-size: 0.9rem;
293
  cursor: pointer;
294
  transition: all 0.2s;
295
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
296

297
  &:hover {
298
    background: $color-gray--lighter;
299
    border-color: $color-gray--base;
300
  }
301
}
302

303
.collections-card {
304
  background: white;
305
  border-radius: 12px;
306
  padding: 16px;
307
  margin-bottom: 16px;
308
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
309
  border-left: 4px solid $color-blue--bright;
310
}
311

312
.collections-title {
313
  display: flex;
314
  align-items: center;
315
  font-size: 1rem;
316
  font-weight: 600;
317
  color: $color-gray--darker;
318
  margin: 0 0 12px 0;
319
}
320

321
.collection-item {
322
  display: flex;
323
  align-items: flex-start;
324
  gap: 12px;
325
  padding: 10px 0;
326
  border-bottom: 1px solid $color-gray--lighter;
327

328
  &:last-child {
329
    border-bottom: none;
330
    padding-bottom: 0;
331
  }
332
}
333

334
.collection-icon {
335
  color: $color-blue--bright;
336
  font-size: 1rem;
337
  margin-top: 2px;
338
}
339

340
.collection-info {
341
  flex: 1;
342
}
343

344
.collection-date {
345
  display: block;
346
  font-weight: 600;
347
  color: $color-black;
348
  margin-bottom: 2px;
349
}
350

351
.collection-details {
352
  font-size: 0.9rem;
353
  color: $color-gray--dark;
354
}
355

356
.posts-container {
357
  display: flex;
358
  flex-direction: column;
359
  gap: 0;
360
}
361

362
.loading-placeholder {
363
  background: white;
364
  margin-bottom: 12px;
365
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
366
}
367

368
.loading-placeholder::before {
369
  content: '';
370
  display: block;
371
  width: 100%;
372
  padding-bottom: 50%;
373
  background: $color-gray--light;
374
}
375

376
.loading-more {
377
  display: flex;
378
  justify-content: center;
379
  padding: 20px;
380
}
381

382
.empty-state {
383
  display: flex;
384
  flex-direction: column;
385
  align-items: center;
386
  justify-content: center;
387
  padding: 48px 24px;
388
  background: white;
389
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
390
}
391

392
.empty-icon {
393
  font-size: 3rem;
394
  color: $color-gray--base;
395
  margin-bottom: 16px;
396
}
397

398
.empty-text {
399
  font-size: 1.1rem;
400
  color: $color-gray--dark;
401
  margin-bottom: 20px;
402
}
403

404
.empty-actions {
405
  display: flex;
406
  gap: 12px;
407
  flex-wrap: wrap;
408
  justify-content: center;
409
}
410

411
.mobile-btn {
412
  display: flex;
413
  align-items: center;
414
  justify-content: center;
415
  padding: 0.6rem 1.5rem;
416
  font-size: 0.9rem;
417
  font-weight: 600;
418
  text-decoration: none;
419
  transition: transform 0.1s;
420

421
  &:active {
422
    transform: scale(0.98);
423
  }
424

425
  &--give {
426
    background: $colour-success;
427
    color: $color-white;
428

429
    &:hover {
430
      background: darken($colour-success, 5%);
431
      color: $color-white;
432
    }
433
  }
434

435
  &--find {
436
    background: $colour-secondary;
437
    color: $color-white;
438

439
    &:hover {
440
      background: darken($colour-secondary, 5%);
441
      color: $color-white;
442
    }
443
  }
444
}
445

446
.minheight {
447
  min-height: 200px;
448
}
449
</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