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

Freegle / iznik-nuxt3 / 362c44c2-e57e-4d64-bd7b-e9153d7dada3

10 Dec 2025 11:02PM UTC coverage: 43.107% (+0.02%) from 43.09%
362c44c2-e57e-4d64-bd7b-e9153d7dada3

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

2831 of 7231 branches covered (39.15%)

Branch coverage included in aggregate %.

0 of 13 new or added lines in 1 file covered. (0.0%)

505 existing lines in 19 files now uncovered.

3398 of 7219 relevant lines covered (47.07%)

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

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

98
const messageStore = useMessageStore()
8✔
99
const userStore = useUserStore()
8✔
100
const trystStore = useTrystStore()
8✔
101

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

109
const emit = defineEmits(['load-more'])
8✔
110

111
const scrollboxHeight = ref(1000)
8✔
112

113
const showOldPosts = ref(false)
8✔
114
function toggleShowOldPosts() {
×
115
  showOldPosts.value = !showOldPosts.value
×
116
}
117

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

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

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

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

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

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

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

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

172
  return result
22✔
173
})
174

175
const upcomingTrysts = computed(() => {
8✔
176
  const ret = []
27✔
177

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

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

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

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

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

221
.my-posts-list {
222
  padding: 0;
223
}
224

225
.active-posts-header {
226
  display: flex;
227
  align-items: center;
228
  justify-content: center;
229
  width: 100%;
230
  padding: 10px 16px;
231
  margin: 0 12px 12px 12px;
232
  background: white;
233
  border: 1px solid $color-gray--light;
234
  color: $colour-success;
235
  font-weight: 500;
236
  font-size: 0.9rem;
237
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
238
}
239

240
.old-posts-toggle {
241
  padding: 8px 12px;
242
  margin-bottom: 12px;
243
}
244

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

261
  &:hover {
262
    background: $color-gray--lighter;
263
    border-color: $color-gray--base;
264
  }
265
}
266

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

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

285
.collection-item {
286
  display: flex;
287
  align-items: flex-start;
288
  gap: 12px;
289
  padding: 10px 0;
290
  border-bottom: 1px solid $color-gray--lighter;
291

292
  &:last-child {
293
    border-bottom: none;
294
    padding-bottom: 0;
295
  }
296
}
297

298
.collection-icon {
299
  color: $color-blue--bright;
300
  font-size: 1rem;
301
  margin-top: 2px;
302
}
303

304
.collection-info {
305
  flex: 1;
306
}
307

308
.collection-date {
309
  display: block;
310
  font-weight: 600;
311
  color: $color-black;
312
  margin-bottom: 2px;
313
}
314

315
.collection-details {
316
  font-size: 0.9rem;
317
  color: $color-gray--dark;
318
}
319

320
.posts-container {
321
  display: flex;
322
  flex-direction: column;
323
  gap: 0;
324
}
325

326
.loading-placeholder {
327
  display: flex;
328
  justify-content: center;
329
  align-items: center;
330
  padding: 40px;
331
  background: white;
332
  border-radius: 12px;
333
  margin-bottom: 12px;
334
}
335

336
.loading-more {
337
  display: flex;
338
  justify-content: center;
339
  padding: 20px;
340
}
341

342
.empty-state {
343
  display: flex;
344
  flex-direction: column;
345
  align-items: center;
346
  justify-content: center;
347
  padding: 48px 24px;
348
  background: white;
349
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
350
}
351

352
.empty-icon {
353
  font-size: 3rem;
354
  color: $color-gray--base;
355
  margin-bottom: 16px;
356
}
357

358
.empty-text {
359
  font-size: 1.1rem;
360
  color: $color-gray--dark;
361
  margin-bottom: 20px;
362
}
363

364
.empty-actions {
365
  display: flex;
366
  gap: 12px;
367
  flex-wrap: wrap;
368
  justify-content: center;
369
}
370

371
.mobile-btn {
372
  display: flex;
373
  align-items: center;
374
  justify-content: center;
375
  padding: 0.6rem 1.5rem;
376
  font-size: 0.9rem;
377
  font-weight: 600;
378
  text-decoration: none;
379
  transition: transform 0.1s;
380

381
  &:active {
382
    transform: scale(0.98);
383
  }
384

385
  &--give {
386
    background: $colour-success;
387
    color: $color-white;
388

389
    &:hover {
390
      background: darken($colour-success, 5%);
391
      color: $color-white;
392
    }
393
  }
394

395
  &--find {
396
    background: $colour-secondary;
397
    color: $color-white;
398

399
    &:hover {
400
      background: darken($colour-secondary, 5%);
401
      color: $color-white;
402
    }
403
  }
404
}
405

406
.minheight {
407
  min-height: 200px;
408
}
409
</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