Skip to content

Commit 057a93d

Browse files
committed
feat(load-more): adds a load more button
1 parent 2fc03b5 commit 057a93d

File tree

10 files changed

+194
-45
lines changed

10 files changed

+194
-45
lines changed

src/components/common/list/ListEmpty.vue

+14-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import { NEmpty } from 'naive-ui';
33
4+
import ListLoadMore from '~/components/common/list/ListLoadMore.vue';
45
import { useI18n } from '~/utils';
56
67
const i18n = useI18n('list', 'empty');
@@ -22,24 +23,25 @@ defineProps({
2223
default: 0,
2324
},
2425
});
26+
27+
const emit = defineEmits<{
28+
(
29+
e: 'onLoadMore',
30+
pagination: { page: number; pageCount: number; pageSize: number },
31+
): void;
32+
}>();
2533
</script>
2634

2735
<template>
2836
<NEmpty size="large" :show-description="false">
2937
<template #extra>
3038
<span class="empty">{{ i18n('no_data') }}</span>
31-
<div v-if="page && pageCount">
32-
<div class="empty">
33-
{{ i18n('pages_line_1') }} <span class="page"> {{ page }} </span>
34-
{{ i18n('pages_line_2') }} <span class="page"> {{ pageCount }} </span>.
35-
</div>
36-
<template v-if="page < pageCount">
37-
<div class="empty">{{ i18n('page_size') }}</div>
38-
<div class="empty">
39-
{{ i18n('current_page_size') }} <span class="page"> {{ pageSize }} </span>.
40-
</div>
41-
</template>
42-
</div>
39+
<ListLoadMore
40+
:page="page"
41+
:page-count="pageCount"
42+
:page-size="pageSize"
43+
@on-load-more="e => emit('onLoadMore', e)"
44+
/>
4345
</template>
4446
</NEmpty>
4547
</template>

src/components/common/list/ListItem.vue

+9-16
Original file line numberDiff line numberDiff line change
@@ -91,23 +91,16 @@ const imageSize = computed(() =>
9191
watch(
9292
item,
9393
_item => {
94-
if (!_item?.poster && _item?.type && _item?.movie?.ids?.tmdb) {
95-
_item.poster = getImageUrl(
96-
{
97-
id: _item.movie.ids.tmdb,
98-
type: _item.type,
99-
},
100-
imageSize.value,
101-
);
102-
} else if (!_item?.poster && _item?.type && _item?.show?.ids?.tmdb) {
103-
_item.poster = getImageUrl(
104-
{
105-
id: _item.show.ids.tmdb,
106-
type: 'show',
107-
},
108-
imageSize.value,
109-
);
94+
if (_item?.poster) return;
95+
if (!_item?.type) return;
96+
97+
const type = _item.type === 'movie' ? 'movie' : 'show';
98+
if (!_item?.[type]?.ids?.tmdb) {
99+
console.warn('No tmdb id found', JSON.parse(JSON.stringify(_item?.[_item?.type])));
100+
return;
110101
}
102+
103+
_item.poster = getImageUrl({ id: _item[type]!.ids.tmdb, type }, imageSize.value);
111104
},
112105
{ immediate: true },
113106
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<script setup lang="ts">
2+
import { NButton } from 'naive-ui';
3+
4+
import { useI18n } from '~/utils';
5+
6+
const i18n = useI18n('list', 'more');
7+
8+
defineProps({
9+
page: {
10+
type: Number,
11+
required: false,
12+
default: 0,
13+
},
14+
pageCount: {
15+
type: Number,
16+
required: false,
17+
default: 0,
18+
},
19+
pageSize: {
20+
type: Number,
21+
required: false,
22+
default: 0,
23+
},
24+
});
25+
26+
const emit = defineEmits<{
27+
(
28+
e: 'onLoadMore',
29+
pagination: { page: number; pageCount: number; pageSize: number },
30+
): void;
31+
}>();
32+
</script>
33+
34+
<template>
35+
<template v-if="page && pageCount">
36+
<div class="row">
37+
{{ i18n('pages_line_1') }} <span class="page"> {{ page }} </span>
38+
{{ i18n('pages_line_2') }} <span class="page"> {{ pageCount }} </span>
39+
{{ i18n('pages_line_3') }}
40+
</div>
41+
<div class="row">
42+
{{ i18n('current_page_size') }} <span class="page"> {{ pageSize }} </span>.
43+
</div>
44+
<template v-if="page < pageCount">
45+
<NButton
46+
class="button"
47+
tertiary
48+
round
49+
type="primary"
50+
@click="emit('onLoadMore', { page, pageCount, pageSize })"
51+
>
52+
Load more Pages
53+
</NButton>
54+
</template>
55+
</template>
56+
</template>
57+
58+
<style scoped lang="scss">
59+
.row {
60+
margin-top: 0.5rem;
61+
color: var(--n-text-color);
62+
transition: color 0.3s var(--n-bezier);
63+
64+
.page {
65+
color: var(--primary-color-disabled);
66+
font-weight: bold;
67+
}
68+
}
69+
70+
.button {
71+
margin: 1.25rem 1.25rem 0.5rem;
72+
}
73+
</style>

src/components/common/list/ListScroll.model.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export type OnScroll = (listRef: Ref<VirtualListRef | undefined>) => void;
1818
export type OnUpdated = (listRef: Ref<VirtualListRef | undefined>) => void;
1919

2020
export type ListScrollItem = {
21-
id: string | number;
21+
id: string | number | 'load-more';
2222
index: number;
2323

2424
type?: 'movie' | 'show' | 'season' | 'episode';

src/components/common/list/ListScroll.vue

+43-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts" setup>
2-
import { NTimeline, NVirtualList } from 'naive-ui';
2+
import { NFlex, NTimeline, NVirtualList } from 'naive-ui';
33
44
import { ref, toRefs } from 'vue';
55
@@ -14,6 +14,7 @@ import type { TraktClientPagination } from '~/models/trakt/trakt-client.model';
1414
1515
import ListEmpty from '~/components/common/list/ListEmpty.vue';
1616
import ListItem from '~/components/common/list/ListItem.vue';
17+
import ListLoadMore from '~/components/common/list/ListLoadMore.vue';
1718
1819
const listRef = ref<VirtualListRef>();
1920
@@ -54,6 +55,15 @@ const emits = defineEmits<{
5455
(e: 'onScrollTop', listRef: Ref<VirtualListRef | undefined>): void;
5556
(e: 'onScroll', listRef: Ref<VirtualListRef | undefined>): void;
5657
(e: 'onUpdated', listRef: Ref<VirtualListRef | undefined>): void;
58+
(
59+
e: 'onloadMore',
60+
payload: {
61+
listRef: Ref<VirtualListRef | undefined>;
62+
page: number;
63+
pageCount: number;
64+
pageSize: number;
65+
},
66+
): void;
5767
}>();
5868
5969
defineExpose({
@@ -90,6 +100,10 @@ const hoverDate = ref<string>();
90100
const onHover = ({ item, hover }: { item: ListScrollItem; hover: boolean }) => {
91101
if (hover) hoverDate.value = item.date?.current?.toDateString();
92102
};
103+
104+
const onLoadMore = (payload: { page: number; pageCount: number; pageSize: number }) => {
105+
emits('onloadMore', { listRef, ...payload });
106+
};
93107
</script>
94108

95109
<template>
@@ -115,7 +129,25 @@ const onHover = ({ item, hover }: { item: ListScrollItem; hover: boolean }) => {
115129
@vue:updated="onUpdatedHandler"
116130
>
117131
<template #default="{ item }">
132+
<NFlex
133+
v-if="item.id === 'load-more'"
134+
class="load-more"
135+
justify="center"
136+
align="center"
137+
vertical
138+
size="small"
139+
:theme-overrides="{ gapSmall: '0' }"
140+
:style="`height: ${listOptions?.itemSize ?? 145}px;`"
141+
>
142+
<ListLoadMore
143+
:page="pagination?.page"
144+
:page-count="pagination?.pageCount"
145+
:page-size="pageSize"
146+
@on-load-more="onLoadMore"
147+
/>
148+
</NFlex>
118149
<ListItem
150+
v-else
119151
:key="item.id"
120152
:item="item"
121153
:index="item.index"
@@ -133,18 +165,28 @@ const onHover = ({ item, hover }: { item: ListScrollItem; hover: boolean }) => {
133165
:page="pagination?.page"
134166
:page-count="pagination?.pageCount"
135167
:page-size="pageSize"
168+
@on-load-more="onLoadMore"
136169
/>
137170
</Transition>
138171
</template>
139172

140173
<style lang="scss" scoped>
141174
@use '~/styles/layout' as layout;
142175
@use '~/styles/transition' as transition;
176+
@use '~/styles/animation' as animation;
143177
@include transition.fade;
178+
@include animation.fade-in;
144179
145180
.list-scroll {
146181
height: calc(100dvh - 8px);
147182
margin-top: -#{layout.$header-navbar-height};
148183
margin-bottom: 8px;
184+
185+
.load-more {
186+
margin-top: 1rem;
187+
opacity: 0;
188+
animation: fade-in 0.5s forwards;
189+
animation-delay: 0.25s;
190+
}
149191
}
150192
</style>

src/components/common/list/useListScroll.ts

+19
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { computed } from 'vue';
33
import type { Ref } from 'vue';
44

55
import type { ListScrollItem } from '~/components/common/list/ListScroll.model';
6+
import type { TraktClientPagination } from '~/models/trakt/trakt-client.model';
67
import type { TraktEpisode } from '~/models/trakt/trakt-episode.model';
78
import type { TraktMovie } from '~/models/trakt/trakt-movie.model';
89
import type { TraktSeason } from '~/models/trakt/trakt-season.model';
@@ -44,3 +45,21 @@ export const useListScroll = <T extends string>(items: Ref<ListScrollSourceItems
4445
return { ..._item, date };
4546
});
4647
});
48+
49+
export const addLoadMore = (
50+
items: Ref<ListScrollItem[]>,
51+
pagination: Ref<TraktClientPagination | undefined>,
52+
search: Ref<string>,
53+
): Ref<ListScrollItem[]> => {
54+
return computed(() => {
55+
const array = items.value;
56+
if (!array.length) return array;
57+
if (!search.value) return array;
58+
if (!pagination.value?.page) return array;
59+
if (!pagination.value?.pageCount) return array;
60+
if (pagination.value.page === pagination.value.pageCount) return array;
61+
if (array.length && array[array.length - 1].id === 'load-more') return array;
62+
const loadMore: ListScrollItem = { id: 'load-more', index: items.value.length };
63+
return [...array, loadMore];
64+
});
65+
};

src/components/views/history/HistoryComponent.vue

+12-6
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ import type {
99
1010
import FloatingButton from '~/components/common/buttons/FloatingButton.vue';
1111
import ListScroll from '~/components/common/list/ListScroll.vue';
12-
import { useListScroll } from '~/components/common/list/useListScroll';
12+
import { addLoadMore, useListScroll } from '~/components/common/list/useListScroll';
1313
import { useHistoryStore, useHistoryStoreRefs } from '~/stores/data/history.store';
1414
import { useUserSettingsStoreRefs } from '~/stores/settings/user.store';
1515
import { useI18n } from '~/utils';
1616
17-
const { filteredHistory, pagination, loading, pageSize, belowThreshold } =
17+
const { filteredHistory, pagination, loading, pageSize, belowThreshold, searchHistory } =
1818
useHistoryStoreRefs();
1919
const { fetchHistory, clearState } = useHistoryStore();
2020
@@ -40,7 +40,9 @@ onMounted(() => {
4040
});
4141
});
4242
43-
const history = useListScroll(filteredHistory, 'watched_at');
43+
const list = useListScroll(filteredHistory, 'watched_at');
44+
45+
const history = addLoadMore(list, pagination, searchHistory);
4446
4547
const onScroll: OnScroll = async listRef => {
4648
const key = history.value[history.value.length - 1].id;
@@ -50,16 +52,19 @@ const onScroll: OnScroll = async listRef => {
5052
listRef.value?.scrollTo({ key, debounce: true });
5153
};
5254
55+
const onLoadMore = async () =>
56+
fetchHistory({
57+
page: pagination.value?.page ? pagination.value.page + 1 : 0,
58+
});
59+
5360
/**
5461
* This is a workaround for the onUpdated lifecycle hook not triggering when wrapped in transition.
5562
*/
5663
const onUpdated: OnUpdated = listRef => {
5764
const { scrollHeight, clientHeight } = listRef.value?.$el?.firstElementChild ?? {};
5865
if (scrollHeight !== clientHeight || !belowThreshold.value || loading.value) return;
5966
60-
return fetchHistory({
61-
page: pagination.value?.page ? pagination.value.page + 1 : 0,
62-
});
67+
return onLoadMore();
6368
};
6469
6570
const listRef = ref<{ list: VirtualListRef }>();
@@ -85,6 +90,7 @@ const onClick = () => {
8590
@on-scroll-top="scrolled = false"
8691
@on-scroll-bottom="onScroll"
8792
@on-updated="onUpdated"
93+
@onload-more="onLoadMore"
8894
>
8995
<template #default>
9096
<!-- TODO buttons here-->

src/i18n/en/list/list.json

+7-7
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,19 @@
33
"message": "No data found.",
44
"description": "Empty message when there is no data to display in the list."
55
},
6-
"list__empty__pages_line_1": {
7-
"message": "Pages searched",
6+
"list__more__pages_line_1": {
7+
"message": "Searched",
88
"description": "First line of the list empty pagination message."
99
},
10-
"list__empty__pages_line_2": {
10+
"list__more__pages_line_2": {
1111
"message": "out of",
1212
"description": "Second line of the list empty pagination message."
1313
},
14-
"list__empty__page_size": {
15-
"message": "Increase the page size to widen the search.",
16-
"description": "Prompt user to increase the page size to widen the search."
14+
"list__more__pages_line_3": {
15+
"message": "pages.",
16+
"description": "Third line of the list empty pagination message."
1717
},
18-
"list__empty__current_page_size": {
18+
"list__more__current_page_size": {
1919
"message": "Current page size is",
2020
"description": "Current page size label."
2121
}

src/stores/data/image.store.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,10 @@ export const useImageStore = defineStore('data.image', () => {
7474
} else throw new Error('Unsupported type or missing parameters for fetchImageUrl');
7575

7676
const fetchedImages = payload.posters ?? payload.stills;
77-
if (!fetchedImages?.length) return;
77+
if (!fetchedImages?.length) {
78+
console.warn('No images found for', { id, season, episode, type });
79+
return;
80+
}
7881
const image = arrayMax(fetchedImages, 'vote_average', i => !!i.file_path)?.file_path;
7982
if (!image) return;
8083
images[type][key] = image;
@@ -84,7 +87,7 @@ export const useImageStore = defineStore('data.image', () => {
8487
const getImageUrl = ({ id, season, episode, type }: ImageQuery, size: string = 'original') => {
8588
if (!tmdbConfig.value) throw new Error('TmdbConfiguration not initialized');
8689
const key = [id, season, episode].filter(Boolean).join('-');
87-
const imageRef = ref<string>(images[type][key]);
90+
const imageRef = computed(() => images[type][key]);
8891
if (!imageRef.value) fetchImageUrl(key, { id, season, episode, type }).catch(console.error);
8992

9093
return computed(() => {

0 commit comments

Comments
 (0)