|
1 | 1 | <script setup lang="ts"> |
| 2 | +import { useInfiniteScroll } from '@vueuse/core' |
2 | 3 | import { updateProfile as updateProfileUtil } from '~/utils/atproto/profile' |
| 4 | +import { fetchProfileLikes } from '~/utils/atproto/likes' |
3 | 5 | import type { CommandPaletteContextCommandInput } from '~/types/command-palette' |
4 | 6 | import { getSafeHttpUrl } from '#shared/utils/url' |
5 | 7 |
|
@@ -79,13 +81,38 @@ async function updateProfile() { |
79 | 81 | } |
80 | 82 | } |
81 | 83 |
|
82 | | -const { data: likes, status } = useProfileLikes(identity) |
| 84 | +const allLikesRecords = ref<string[]>([]) |
| 85 | +// undefined = not yet fetched, string = next page cursor, null = no more pages |
| 86 | +const likesCursor = shallowRef<string | null | undefined>(undefined) |
| 87 | +const likesError = shallowRef(false) |
| 88 | +
|
| 89 | +const hasMoreLikes = computed(() => typeof likesCursor.value === 'string') |
| 90 | +const isLoadingInitialLikes = computed(() => likesCursor.value === undefined && !likesError.value) |
| 91 | +
|
| 92 | +const { isLoading: likesLoadingMore } = useInfiniteScroll( |
| 93 | + () => (import.meta.client ? window : null), |
| 94 | + async () => { |
| 95 | + try { |
| 96 | + const result = await fetchProfileLikes(identity.value, likesCursor.value ?? null, 20) |
| 97 | + allLikesRecords.value = [...allLikesRecords.value, ...(result.likes ?? [])] |
| 98 | + likesCursor.value = result.cursor ?? null |
| 99 | + } catch { |
| 100 | + likesError.value = true |
| 101 | + } |
| 102 | + }, |
| 103 | + { |
| 104 | + distance: 200, |
| 105 | + // undefined (initial) → allow load; null (exhausted) → stop |
| 106 | + canLoadMore: () => likesCursor.value !== null, |
| 107 | + }, |
| 108 | +) |
83 | 109 |
|
84 | 110 | const showInviteSection = computed(() => { |
85 | 111 | return ( |
86 | 112 | profile.value.recordExists === false && |
87 | | - status.value === 'success' && |
88 | | - !likes.value?.records?.length && |
| 113 | + !likesError.value && |
| 114 | + allLikesRecords.value.length === 0 && |
| 115 | + likesCursor.value !== undefined && |
89 | 116 | !userPending.value && |
90 | 117 | user.value?.handle !== profile.value.handle |
91 | 118 | ) |
@@ -239,18 +266,36 @@ defineOgImage( |
239 | 266 | dir="ltr" |
240 | 267 | > |
241 | 268 | {{ $t('profile.likes') }} |
242 | | - <span v-if="likes">({{ likes.records?.length ?? 0 }})</span> |
| 269 | + <span>({{ allLikesRecords.length ?? 0 }})</span> |
243 | 270 | </h2> |
244 | | - <div v-if="status === 'pending'" class="grid grid-cols-1 lg:grid-cols-2 gap-4"> |
| 271 | + <div v-if="isLoadingInitialLikes" class="flex flex-col gap-4"> |
245 | 272 | <SkeletonBlock v-for="i in 4" :key="i" class="h-16 rounded-lg" /> |
246 | 273 | </div> |
247 | | - <div v-else-if="status === 'error'"> |
| 274 | + <div v-else-if="likesError"> |
248 | 275 | <p>{{ $t('common.error') }}</p> |
249 | 276 | </div> |
250 | | - <div v-else-if="likes?.records" class="grid grid-cols-1 lg:grid-cols-2 gap-4"> |
251 | | - <PackageLikeCard v-for="like in likes.records" :packageUrl="like.value.subjectRef" /> |
| 277 | + <template v-else-if="allLikesRecords.length > 0"> |
| 278 | + <ol class="list-none m-0 p-0 grid grid-cols-1 lg:grid-cols-2 gap-4"> |
| 279 | + <li v-for="like in allLikesRecords" :key="like"> |
| 280 | + <PackageLikeCard :packageUrl="like" /> |
| 281 | + </li> |
| 282 | + </ol> |
| 283 | + </template> |
| 284 | + |
| 285 | + <!-- Loading more indicator --> |
| 286 | + <div v-if="likesLoadingMore" class="flex items-center justify-center py-4 gap-2"> |
| 287 | + <span class="i-svg-spinners:ring-resize w-4 h-4" aria-hidden="true" /> |
| 288 | + <span class="text-fg-muted text-sm">{{ $t('common.loading_more') }}</span> |
252 | 289 | </div> |
253 | 290 |
|
| 291 | + <!-- End of results --> |
| 292 | + <p |
| 293 | + v-else-if="!hasMoreLikes && allLikesRecords.length > 0" |
| 294 | + class="py-4 text-center text-fg-subtle font-mono text-sm" |
| 295 | + > |
| 296 | + {{ $t('common.end_of_results') }} |
| 297 | + </p> |
| 298 | + |
254 | 299 | <!-- Invite section: shown when user does not have npmx profile or any like lexicons --> |
255 | 300 | <div |
256 | 301 | v-if="showInviteSection" |
|
0 commit comments