Skip to content

Commit 4c92c54

Browse files
committed
feat(search): implement search page
1 parent 3b3e93c commit 4c92c54

13 files changed

+256
-49
lines changed

src/components/common/list/ListItem.vue

+17-4
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,16 @@ const emit = defineEmits<{
8080
8181
const i18n = useI18n('list', 'item');
8282
83-
const { item, noHeader, nextHasHeader, poster, episode, hideDate, scrollIntoView } =
84-
toRefs(props);
83+
const {
84+
item,
85+
noHeader,
86+
nextHasHeader,
87+
poster,
88+
episode,
89+
hideDate,
90+
scrollIntoView,
91+
height,
92+
} = toRefs(props);
8593
8694
const onHover = (_hover: boolean) => {
8795
emit('onHover', { item: item?.value, hover: _hover });
@@ -153,6 +161,8 @@ onBeforeUnmount(() => {
153161
});
154162
});
155163
164+
const itemHeight = computed(() => (height?.value ? `${height.value}px` : undefined));
165+
156166
const ListScrollItemTypeLocal = ListScrollItemType;
157167
</script>
158168

@@ -167,7 +177,7 @@ const ListScrollItemTypeLocal = ListScrollItemType;
167177
'show-date': !hideDate,
168178
}"
169179
:style="{
170-
'--list-item-height': height ? `${height}px` : undefined,
180+
'--list-item-height': itemHeight,
171181
}"
172182
:data-key="item.id"
173183
:data-index="item.index"
@@ -222,7 +232,10 @@ const ListScrollItemTypeLocal = ListScrollItemType;
222232
<NImage
223233
alt="poster-image"
224234
class="poster"
225-
:class="{ episode, loading: !imgLoaded }"
235+
:class="{
236+
episode,
237+
loading: !imgLoaded,
238+
}"
226239
:object-fit="objectFit"
227240
width="100%"
228241
lazy

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

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { NVirtualList, TagProps, VirtualListInst } from 'naive-ui';
22

33
import type { Ref } from 'vue';
44
import type { TraktEpisode } from '~/models/trakt/trakt-episode.model';
5+
import type { TraktList } from '~/models/trakt/trakt-list.model';
56
import type { TraktMovie } from '~/models/trakt/trakt-movie.model';
67
import type { TraktPerson } from '~/models/trakt/trakt-people.model';
78
import type { TraktSeason } from '~/models/trakt/trakt-season.model';
@@ -30,6 +31,7 @@ export type ListScrollSourceItem = {
3031
season?: TraktSeason<'short'>;
3132
episode?: TraktEpisode<'short'>;
3233
person?: TraktPerson<'short'>;
34+
list?: TraktList<'short'>;
3335
};
3436

3537
export type ListScrollItemTag = {
@@ -46,6 +48,7 @@ export const ListScrollItemType = {
4648
season: 'season',
4749
episode: 'episode',
4850
person: 'person',
51+
list: 'list',
4952
loading: 'loading',
5053
loadMore: 'load-more',
5154
placeholder: 'placeholder',

src/components/common/list/use-list-scroll.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const getDate = <D extends string, T extends ListScrollSourceItemWithDate
5151
const _date = typeof dateFn === 'function' ? dateFn(media) : media[dateFn];
5252
if (!_date) return;
5353
const date: ListScrollItem['date'] = { current: new Date(_date) };
54+
if (date.current.toString() === 'Invalid Date') return;
5455
const previous = typeof dateFn === 'function' ? dateFn(array[index - 1]) : array[index - 1]?.[dateFn];
5556
if (index > 0 && previous) date.previous = new Date(previous);
5657
const next = typeof dateFn === 'function' ? dateFn(array[index + 1]) : array[index + 1]?.[dateFn];
@@ -84,9 +85,9 @@ export const getPosterQuery =
8485
} satisfies ImageQuery;
8586
};
8687

87-
export const useListScroll = <D extends string, T extends ListScrollSourceItemWithDate<D>>(
88+
export const useListScroll = <T extends ListScrollSourceItemWithDate<D>, D extends string | never = never>(
8889
items: Ref<T[]>,
89-
dateFn?: D | ((item: T) => ListScrollSourceItemWithDate<D>[D]),
90+
dateFn?: D | ((item: T) => T[D]),
9091
) => {
9192
return computed<ListScrollItem[]>(() => {
9293
const array = items.value;
@@ -122,7 +123,7 @@ export const useListScrollEvents = (
122123
pagination,
123124
loading,
124125
belowThreshold,
125-
}: { data: Ref<ListScrollItem[]>; pagination: Ref<TraktClientPagination | undefined>; loading: Ref<boolean>; belowThreshold: Ref<boolean> },
126+
}: { data: Ref<ListScrollItem[]>; pagination: Ref<TraktClientPagination | undefined>; loading: Ref<boolean>; belowThreshold?: Ref<boolean> },
126127
) => {
127128
const onScroll: OnScroll = async listRef => {
128129
const key = data.value[data.value.length - 1].id;
@@ -142,7 +143,7 @@ export const useListScrollEvents = (
142143
*/
143144
const onUpdated: OnUpdated = listRef => {
144145
const { scrollHeight, clientHeight } = listRef.value?.$el?.firstElementChild ?? {};
145-
if (scrollHeight !== clientHeight || !belowThreshold.value || loading.value) return;
146+
if (scrollHeight !== clientHeight || !belowThreshold?.value || loading.value) return;
146147

147148
return onLoadMore();
148149
};
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,66 @@
11
<script lang="ts" setup>
2-
// TODO
2+
import FloatingButton from '~/components/common/buttons/FloatingButton.vue';
3+
import { useBackToTop } from '~/components/common/buttons/use-back-to-top';
4+
import ListScroll from '~/components/common/list/ListScroll.vue';
5+
6+
import {
7+
useListScroll,
8+
useListScrollEvents,
9+
} from '~/components/common/list/use-list-scroll';
10+
import {
11+
type SearchResult,
12+
useSearchStore,
13+
useSearchStoreRefs,
14+
} from '~/stores/data/search.store';
15+
import { useI18n } from '~/utils';
16+
import { watchUserChange } from '~/utils/store.utils';
17+
18+
const i18n = useI18n('search');
19+
20+
const { searchResults, loading, pagination } = useSearchStoreRefs();
21+
const { fetchSearchResults, clearState } = useSearchStore();
22+
23+
watchUserChange({
24+
fetch: fetchSearchResults,
25+
clear: clearState,
26+
});
27+
28+
const list = useListScroll<SearchResult>(searchResults);
29+
30+
const { onScroll } = useListScrollEvents(fetchSearchResults, {
31+
data: list,
32+
pagination,
33+
loading,
34+
});
35+
36+
const { scrolled, listRef, onClick } = useBackToTop();
337
</script>
438

539
<template>
6-
<span>This is a search component</span>
40+
<div class="container">
41+
<ListScroll
42+
ref="listRef"
43+
hide-date
44+
:items="list"
45+
:loading="loading"
46+
:scroll-threshold="300"
47+
@on-scroll="scrolled = true"
48+
@on-scroll-bottom="onScroll"
49+
>
50+
<template #default>
51+
<!-- TODO buttons here-->
52+
</template>
53+
</ListScroll>
54+
55+
<FloatingButton :show="scrolled" @on-click="onClick">
56+
{{ i18n('back_to_top', 'common', 'button') }}
57+
</FloatingButton>
58+
</div>
759
</template>
860

961
<style lang="scss" scoped>
10-
// TODO
62+
.container {
63+
width: 100%;
64+
height: 100%;
65+
}
1166
</style>

src/components/views/search/SearchNavbar.vue

+12-10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { type Component, computed, defineProps, h, ref } from 'vue';
1313
1414
import type { TraktSearchType } from '~/models/trakt/trakt-search.model';
1515
16+
import NavbarPageSizeSelect from '~/components/common/navbar/NavbarPageSizeSelect.vue';
1617
import IconAccount from '~/components/icons/IconAccount.vue';
1718
import IconChevronDown from '~/components/icons/IconChevronDownSmall.vue';
1819
import IconChevronUp from '~/components/icons/IconChevronUpSmall.vue';
@@ -22,20 +23,18 @@ import IconMovie from '~/components/icons/IconMovie.vue';
2223
import IconScreen from '~/components/icons/IconScreen.vue';
2324
import IconYoutube from '~/components/icons/IconYoutube.vue';
2425
26+
import { SupportedSearchType, useSearchStoreRefs } from '~/stores/data/search.store';
2527
import { useI18n } from '~/utils';
2628
import { debounce } from '~/utils/debounce.utils';
2729
import { useDebouncedSearch } from '~/utils/store.utils';
2830
2931
const i18n = useI18n('navbar', 'search');
3032
31-
const DefaultSearchType: TraktSearchType[] = ['episode', 'show', 'movie', 'person'];
33+
const { search, types, query, pageSize, loading } = useSearchStoreRefs();
3234
33-
const search = ref('');
34-
const escape = ref(false);
35-
const types = ref<TraktSearchType[]>(DefaultSearchType);
36-
const typeOptions = ref<TraktSearchType[]>(DefaultSearchType);
35+
const typeOptions = ref<TraktSearchType[]>(SupportedSearchType);
3736
38-
const debouncedSearch = useDebouncedSearch(search);
37+
const debouncedSearch = useDebouncedSearch(search, 800);
3938
4039
defineProps({
4140
parentElement: {
@@ -155,8 +154,7 @@ const hideTooltip = () => {
155154
placement="bottom"
156155
trigger="focus"
157156
:show-arrow="false"
158-
ò
159-
:disabled="!escape"
157+
:disabled="!query"
160158
:delay="100"
161159
:to="parentElement"
162160
@mouseenter="showTooltip()"
@@ -166,13 +164,15 @@ const hideTooltip = () => {
166164
<NFlex class="list" vertical>
167165
<NFlex v-for="{ key, value } in specials" :key="key" :cols="2" :wrap="false">
168166
<span class="value">{{ value }}</span>
169-
<span>{{ i18n(`tooltip_${ key }`) }}</span>
167+
<span>{{ i18n('tooltip_' + key) }}</span>
170168
</NFlex>
171169
</NFlex>
172170
<template #trigger>
173171
<NInput
174172
v-model:value="debouncedSearch"
175173
class="search-input"
174+
:loading="loading"
175+
:disabled="loading"
176176
:placeholder="i18n('search', 'navbar')"
177177
autosize
178178
clearable
@@ -184,8 +184,10 @@ const hideTooltip = () => {
184184
</template>
185185
</NTooltip>
186186

187+
<NavbarPageSizeSelect v-model:page-size="pageSize" :parent-element="parentElement" />
187188
<NSwitch
188-
v-model:value="escape"
189+
v-if="false"
190+
v-model:value="query"
189191
class="search-switch"
190192
:theme-overrides="{
191193
buttonColor: 'var(--search-switch-button-color)',

src/components/views/watchlist/WatchlistComponent.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ watchUserChange({
2828
clear: clearState,
2929
});
3030
31-
const list = useListScroll<AnyListDateTypes, AnyList>(
31+
const list = useListScroll<AnyList, AnyListDateTypes>(
3232
filteredListItems,
3333
anyListDateGetter,
3434
);

src/models/trakt/trakt-search.model.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import type { TraktApiExtended, TraktApiParams, TraktApiParamsExtended, TraktApiParamsPagination } from '~/models/trakt/trakt-client.model';
2-
import type { Any, EntityTypes } from '~/models/trakt/trakt-entity.model';
2+
import type { Any, EntityTypes, Short } from '~/models/trakt/trakt-entity.model';
33
import type { TraktEpisode } from '~/models/trakt/trakt-episode.model';
44
import type { TraktList } from '~/models/trakt/trakt-list.model';
55
import type { TraktMovie } from '~/models/trakt/trakt-movie.model';
66
import type { TraktPerson } from '~/models/trakt/trakt-people.model';
77
import type { TraktShow } from '~/models/trakt/trakt-show.model';
88
import type { TraktApiCommonFilters } from '~/services/trakt-client/api/trakt-api.filters';
9-
import type { RequireAtLeastOne } from '~/utils/typescript.utils';
109

1110
export type TraktSearchField = {
1211
movie: 'title' | 'overview' | 'aliases' | 'people' | 'translations' | 'tagline';
@@ -53,7 +52,7 @@ type BaseTraktSearchResultItem<I extends EntityTypes = Any> = {
5352
*
5453
* @see [search]{@link https://trakt.docs.apiary.io/#reference/search}
5554
*/
56-
export type TraktSearchResult<T extends 'movie' | 'show' | 'episode' | 'person' | 'list' | Any = Any, I extends EntityTypes = Any> = {
55+
export type TraktSearchResult<T extends 'movie' | 'show' | 'episode' | 'person' | 'list' | Any = Any, I extends EntityTypes = Short> = {
5756
score: number;
5857
type: T extends Any ? 'movie' | 'show' | 'episode' | 'person' | 'list' : T;
5958
} & (T extends 'movie'
@@ -66,7 +65,7 @@ export type TraktSearchResult<T extends 'movie' | 'show' | 'episode' | 'person'
6665
? Pick<BaseTraktSearchResultItem<I>, 'person'>
6766
: T extends 'list'
6867
? Pick<BaseTraktSearchResultItem<I>, 'list'>
69-
: { show: TraktShow<I>; episode: TraktEpisode<I> } | RequireAtLeastOne<Omit<BaseTraktSearchResultItem<I>, 'episode'>>);
68+
: Partial<BaseTraktSearchResultItem<I>>);
7069

7170
export type TraktIdLookupType = 'trakt' | 'imdb' | 'tmdb' | 'tvdb';
7271

src/services/trakt.service.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ export class TraktService {
261261
}
262262

263263
static async search(query: TraktSearch) {
264-
const response = await this.traktClient.search.text(query);
265-
return response.json();
264+
const response = await this.traktClient.search.text.cached(query, undefined, { retention: CacheRetention.Day });
265+
return { data: await response.json(), pagination: response.pagination };
266266
}
267267
}

src/stores/data/image.store.ts

+8-5
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,8 @@ export const useImageStore = defineStore('data.image', () => {
103103
};
104104

105105
const getKeyAndType = ({ id, season, episode, type }: ImageQuery): { key: string; type: ImageQuery['type'] } => {
106-
if (type === 'episode' && season && episode) return { key: `${type}-${id}-${season}-${episode}`, type };
107-
if (['episode', 'season'].includes(type) && season) return { key: `${type}-${id}-${season}`, type: 'season' };
106+
if (type === 'episode' && season !== undefined && episode !== undefined) return { key: `${type}-${id}-${season}-${episode}`, type };
107+
if (['episode', 'season'].includes(type) && season !== undefined) return { key: `${type}-${id}-${season}`, type: 'season' };
108108
if (['episode', 'season', 'show'].includes(type)) return { key: `${type}-${id}`, type: 'show' };
109109
return { key: `${type}-${id}`, type };
110110
};
@@ -118,13 +118,16 @@ export const useImageStore = defineStore('data.image', () => {
118118
payload = await queueRequest(key, () => TraktService.posters.movie(id));
119119
} else if (type === 'person') {
120120
payload = await queueRequest(key, () => TraktService.posters.person(id));
121-
} else if (type === 'episode' && season && episode) {
121+
} else if (type === 'episode' && season !== undefined && episode !== undefined) {
122122
payload = await queueRequest(key, () => TraktService.posters.episode(id, season, episode));
123-
} else if (type === 'season' && season) {
123+
} else if (type === 'season' && season !== undefined) {
124124
payload = await queueRequest(key, () => TraktService.posters.season(id, season));
125125
} else if (type === 'show') {
126126
payload = await queueRequest(key, () => TraktService.posters.show(id));
127-
} else throw new Error('Unsupported type or missing parameters for fetchImageUrl');
127+
} else {
128+
console.error('Unsupported type or missing parameters for fetchImageUrl', { key, id, season, episode, type });
129+
throw new Error('Unsupported type or missing parameters for fetchImageUrl');
130+
}
128131

129132
if ((type === 'episode' && !payload.stills?.length) || (type === 'season' && !payload.posters?.length)) {
130133
const sType = 'show';

src/stores/data/list.store.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { useUserSettingsStoreRefs } from '~/stores/settings/user.store';
1414
import { storage } from '~/utils/browser/browser-storage.utils';
1515
import { debounceLoading, useBelowThreshold, useLoadingPlaceholder, useSearchFilter } from '~/utils/store.utils';
1616

17-
export type AnyList = TraktListItem | TraktWatchlist | TraktFavoriteItem | (TraktCollection & { id: number });
17+
export type AnyList = TraktListItem | TraktWatchlist | TraktFavoriteItem | (TraktCollection & { id: number | string; type?: 'loading' });
1818
export type ListType = {
1919
type: 'list' | 'collaboration' | 'collection' | 'watchlist' | 'favorites';
2020
name: string;
@@ -196,13 +196,13 @@ export const useListStore = defineStore('data.list', () => {
196196
}
197197
const newData = response.data.map((item, index) => {
198198
if ('id' in item) return item;
199-
return { ...item, id: index };
199+
return { ...item, id: `${page}-${index}` };
200200
});
201201
pagination.value = response.pagination;
202-
listItems.value = page ? [...listItems.value.filter(l => l.id >= 0), ...newData] : newData;
202+
listItems.value = page ? [...listItems.value.filter(l => l.type !== 'loading'), ...newData] : newData;
203203
} catch (e) {
204204
console.error('Failed to fetch list');
205-
listItems.value = listItems.value.filter(l => l.id >= 0);
205+
listItems.value = listItems.value.filter(l => l.type !== 'loading');
206206
throw e;
207207
} finally {
208208
clearTimeout(timeout);

0 commit comments

Comments
 (0)