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

Freegle / iznik-nuxt3 / 38f66207-6066-47b4-bc2e-4c69e60182e5

03 Dec 2025 11:02PM UTC coverage: 42.1% (-6.5%) from 48.587%
38f66207-6066-47b4-bc2e-4c69e60182e5

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

2800 of 7561 branches covered (37.03%)

Branch coverage included in aggregate %.

248 of 640 new or added lines in 45 files covered. (38.75%)

582 existing lines in 34 files now uncovered.

3448 of 7280 relevant lines covered (47.36%)

22.66 hits per line

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

49.14
/components/MyPostsPostsList.vue
1
<template>
2
  <div class="my-posts-list">
42✔
3
    <!-- Old posts toggle button -->
4
    <div v-if="oldPosts.length > 0" class="old-posts-toggle">
5
      <button class="toggle-btn" @click="toggleShowOldPosts">
6
        <v-icon :icon="showOldPosts ? 'eye-slash' : 'eye'" class="me-2" />
×
7
        {{ showOldPosts ? 'Hide' : 'Show' }} {{ formattedOldPostsCount }}
×
8
      </button>
9
    </div>
10

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

31
    <!-- Posts list -->
32
    <div v-if="visiblePosts.length > 0" class="posts-container">
42✔
33
      <div v-for="post in visiblePosts" :key="'post-' + post.id">
25✔
34
        <!-- Mobile view -->
35
        <VisibleWhen :at="['xs', 'sm', 'md']">
36
          <Suspense>
37
            <MyMessageMobile
38
              :id="post.id"
39
              :show-old="showOldPosts"
40
              :expand="defaultExpanded"
41
            />
42
            <template #fallback>
43
              <div class="loading-placeholder">
44
                <b-img lazy src="/loader.gif" alt="Loading" width="80px" />
45
              </div>
46
            </template>
47
          </Suspense>
48
        </VisibleWhen>
49
        <!-- Desktop view -->
50
        <VisibleWhen :at="['lg', 'xl', 'xxl']">
51
          <Suspense>
52
            <MyMessage
53
              :id="post.id"
54
              :show-old="showOldPosts"
55
              :expand="defaultExpanded"
56
              class="minheight"
57
            />
58
            <template #fallback>
59
              <div class="loading-placeholder">
60
                <b-img lazy src="/loader.gif" alt="Loading" width="80px" />
61
              </div>
62
            </template>
63
          </Suspense>
64
        </VisibleWhen>
65
      </div>
66
      <div v-if="loading" class="loading-more">
67
        <b-img lazy src="/loader.gif" alt="Loading..." width="60px" />
68
      </div>
69
      <InfiniteLoading
70
        :distance="scrollboxHeight"
71
        @infinite="(event) => emit('load-more', event)"
33✔
72
      />
73
    </div>
74

75
    <!-- Empty state -->
76
    <div v-else class="empty-state">
77
      <div class="empty-icon">
78
        <v-icon icon="folder-open" />
79
      </div>
80
      <p class="empty-text">You have no active posts.</p>
25✔
81
      <div class="empty-actions">
82
        <template v-if="props.type === 'Offer'">
17!
83
          <nuxt-link to="/give" class="action-link action-link--primary">
84
            <v-icon icon="gift" class="me-2" />OFFER something
×
85
          </nuxt-link>
86
        </template>
87
        <template v-else-if="props.type === 'Wanted'">
17!
88
          <nuxt-link to="/find" class="action-link action-link--primary">
89
            <v-icon icon="search" class="me-2" />Ask for something
×
90
          </nuxt-link>
91
        </template>
92
        <template v-else>
93
          <nuxt-link to="/give" class="action-link action-link--offer">
94
            <v-icon icon="gift" class="me-2" />OFFER
22✔
95
          </nuxt-link>
96
          <nuxt-link to="/find" class="action-link action-link--wanted">
97
            <v-icon icon="search" class="me-2" />WANTED
22✔
98
          </nuxt-link>
99
        </template>
100
      </div>
101
    </div>
102
  </div>
103
</template>
104
<script setup>
105
import pluralize from 'pluralize'
106
import dayjs from 'dayjs'
107
import MyMessage from '~/components/MyMessage.vue'
108
import MyMessageMobile from '~/components/MyMessageMobile.vue'
109
import InfiniteLoading from '~/components/InfiniteLoading.vue'
110
import VisibleWhen from '~/components/VisibleWhen'
111
import { useMessageStore } from '~/stores/message'
112
import { useUserStore } from '~/stores/user'
113
import { useTrystStore } from '~/stores/tryst'
114

115
const messageStore = useMessageStore()
8✔
116
const userStore = useUserStore()
8✔
117
const trystStore = useTrystStore()
8✔
118

119
const props = defineProps({
8✔
120
  posts: { type: Array, required: true },
121
  loading: { type: Boolean, required: true },
122
  defaultExpanded: { type: Boolean, required: true },
123
  show: { type: Number, required: true },
124
})
125

126
const emit = defineEmits(['load-more'])
8✔
127

128
const scrollboxHeight = ref(1000)
8✔
129

130
const showOldPosts = ref(false)
8✔
131
function toggleShowOldPosts() {
×
132
  showOldPosts.value = !showOldPosts.value
×
133
}
134

135
// Posts are now passed directly as props
136
const posts = computed(() => {
8✔
137
  return props.posts || []
22!
138
})
139

140
// old posts are those with an outcome
141
const oldPosts = computed(() => {
8✔
142
  return posts.value.filter((post) => post.hasoutcome)
22✔
143
})
144

145
const formattedOldPostsCount = computed(() => {
8✔
146
  return pluralize(`old post`, oldPosts.value.length, true)
×
147
})
148

149
const activePosts = computed(() => {
8✔
150
  const result = posts.value.filter((post) => !post.hasoutcome)
22✔
151
  console.log('DEBUG: activePosts computed', {
22✔
152
    allPostsLength: posts.value.length,
153
    activePostsLength: result.length,
154
    oldPostsLength: posts.value.filter((post) => post.hasoutcome).length,
155
  })
156
  return result
22✔
157
})
158

159
watch(activePosts, (newVal) => {
8✔
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) => {
14✔
163
    if (
8!
164
      post.type === 'Offer' &&
13!
165
      post.promised &&
166
      !post.hasoutcome &&
167
      !messageStore.byId(post.id)
168
    ) {
169
      messageStore.fetch(post.id)
×
170
    }
171
  })
172
})
173

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

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

191
  return result
22✔
192
})
193

194
const upcomingTrysts = computed(() => {
8✔
195
  const ret = []
31✔
196

197
  activePosts.value.forEach((post) => {
31✔
198
    const message = messageStore.byId(post.id)
17✔
199
    if (post.type === 'Offer' && message?.promises?.length) {
17!
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) => {
31✔
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
.old-posts-toggle {
245
  padding: 8px 12px;
246
  margin-bottom: 12px;
247
}
248

249
.toggle-btn {
250
  display: flex;
251
  align-items: center;
252
  justify-content: center;
253
  width: 100%;
254
  padding: 10px 16px;
255
  background: white;
256
  border: 1px solid $color-gray--light;
257
  border-radius: 8px;
258
  color: $color-gray--dark;
259
  font-weight: 500;
260
  font-size: 0.9rem;
261
  cursor: pointer;
262
  transition: all 0.2s;
263
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
264

265
  &:hover {
266
    background: $color-gray--lighter;
267
    border-color: $color-gray--base;
268
  }
269
}
270

271
.collections-card {
272
  background: white;
273
  border-radius: 12px;
274
  padding: 16px;
275
  margin-bottom: 16px;
276
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
277
  border-left: 4px solid $color-blue--bright;
278
}
279

280
.collections-title {
281
  display: flex;
282
  align-items: center;
283
  font-size: 1rem;
284
  font-weight: 600;
285
  color: $color-gray--darker;
286
  margin: 0 0 12px 0;
287
}
288

289
.collection-item {
290
  display: flex;
291
  align-items: flex-start;
292
  gap: 12px;
293
  padding: 10px 0;
294
  border-bottom: 1px solid $color-gray--lighter;
295

296
  &:last-child {
297
    border-bottom: none;
298
    padding-bottom: 0;
299
  }
300
}
301

302
.collection-icon {
303
  color: $color-blue--bright;
304
  font-size: 1rem;
305
  margin-top: 2px;
306
}
307

308
.collection-info {
309
  flex: 1;
310
}
311

312
.collection-date {
313
  display: block;
314
  font-weight: 600;
315
  color: $color-black;
316
  margin-bottom: 2px;
317
}
318

319
.collection-details {
320
  font-size: 0.9rem;
321
  color: $color-gray--dark;
322
}
323

324
.posts-container {
325
  display: flex;
326
  flex-direction: column;
327
  gap: 0;
328
}
329

330
.loading-placeholder {
331
  display: flex;
332
  justify-content: center;
333
  align-items: center;
334
  padding: 40px;
335
  background: white;
336
  border-radius: 12px;
337
  margin-bottom: 12px;
338
}
339

340
.loading-more {
341
  display: flex;
342
  justify-content: center;
343
  padding: 20px;
344
}
345

346
.empty-state {
347
  display: flex;
348
  flex-direction: column;
349
  align-items: center;
350
  justify-content: center;
351
  padding: 48px 24px;
352
  background: white;
353
  border-radius: 12px;
354
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
355
}
356

357
.empty-icon {
358
  width: 80px;
359
  height: 80px;
360
  border-radius: 50%;
361
  background: $color-gray--lighter;
362
  display: flex;
363
  align-items: center;
364
  justify-content: center;
365
  margin-bottom: 16px;
366

367
  svg {
368
    font-size: 2rem;
369
    color: $color-gray--base;
370
  }
371
}
372

373
.empty-text {
374
  font-size: 1.1rem;
375
  color: $color-gray--dark;
376
  margin-bottom: 20px;
377
}
378

379
.empty-actions {
380
  display: flex;
381
  gap: 12px;
382
  flex-wrap: wrap;
383
  justify-content: center;
384
}
385

386
.action-link {
387
  display: inline-flex;
388
  align-items: center;
389
  padding: 12px 24px;
390
  border-radius: 25px;
391
  font-weight: 600;
392
  font-size: 1rem;
393
  text-decoration: none;
394
  transition: all 0.2s;
395

396
  &--primary {
397
    background: $color-green-background;
398
    color: white;
399

400
    &:hover {
401
      background: darken($color-green-background, 10%);
402
      color: white;
403
    }
404
  }
405

406
  &--offer {
407
    background: $color-green-background;
408
    color: white;
409

410
    &:hover {
411
      background: darken($color-green-background, 10%);
412
      color: white;
413
    }
414
  }
415

416
  &--wanted {
417
    background: $color-blue--bright;
418
    color: white;
419

420
    &:hover {
421
      background: darken($color-blue--bright, 10%);
422
      color: white;
423
    }
424
  }
425
}
426

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