Skip to content

Commit e943b1f

Browse files
committed
feat(search): adds search memory & history
1 parent 87bd76a commit e943b1f

File tree

4 files changed

+104
-23
lines changed

4 files changed

+104
-23
lines changed

src/components/container/ContainerComponent.ce.vue

+1
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ const root = ref<HTMLElement>();
151151
}
152152
}
153153
154+
.n-popselect-menu,
154155
.n-select-menu {
155156
@include mixin.hover-background(var(--bg-color-20), var(--bg-color-80));
156157

src/components/views/search/SearchNavbar.vue

+51-15
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
NFlex,
44
NIcon,
55
NInput,
6+
NPopselect,
67
NSelect,
78
NSwitch,
89
NTooltip,
@@ -30,11 +31,37 @@ import { useDebouncedSearch } from '~/utils/store.utils';
3031
3132
const i18n = useI18n('navbar', 'search');
3233
33-
const { search, types, query, pageSize, loading } = useSearchStoreRefs();
34+
const { search, types, query, pageSize, loading, history } = useSearchStoreRefs();
3435
3536
const typeOptions = ref<TraktSearchType[]>(SupportedSearchType);
3637
37-
const debouncedSearch = useDebouncedSearch(search, 800);
38+
const inputFocus = ref(false);
39+
const toggleFocus = (focus: boolean) => {
40+
inputFocus.value = focus;
41+
};
42+
43+
const debouncedSearch = useDebouncedSearch(search, 1000);
44+
45+
const filteredHistory = computed(() =>
46+
[...history.value]
47+
.filter(val => {
48+
const _search = debouncedSearch.value?.toLowerCase().trim();
49+
if (!_search) return false;
50+
const _val = val.toLowerCase().trim();
51+
if (_val === _search) return false;
52+
return _val.includes(_search) || _search.includes(_val);
53+
})
54+
.slice(0, 10),
55+
);
56+
57+
const historyOptions = computed(() =>
58+
filteredHistory.value.map(value => ({ value, label: value })),
59+
);
60+
61+
const showHistory = computed(() => {
62+
if (!inputFocus.value) return false;
63+
return !!filteredHistory.value.length;
64+
});
3865
3966
defineProps({
4067
parentElement: {
@@ -131,7 +158,7 @@ const hideTooltip = () => {
131158
const inputRef = ref();
132159
133160
onActivated(() => {
134-
inputRef.value?.focus();
161+
if (!search.value) inputRef.value?.focus();
135162
});
136163
</script>
137164

@@ -174,20 +201,29 @@ onActivated(() => {
174201
</NFlex>
175202
</NFlex>
176203
<template #trigger>
177-
<NInput
178-
ref="inputRef"
204+
<NPopselect
179205
v-model:value="debouncedSearch"
180-
class="search-input"
181-
:loading="loading"
182-
:disabled="loading"
183-
:placeholder="i18n('search', 'navbar')"
184-
autosize
185-
clearable
206+
:options="historyOptions"
207+
:to="parentElement"
208+
:show="showHistory"
186209
>
187-
<template #prefix>
188-
<NIcon :component="IconLoop" />
189-
</template>
190-
</NInput>
210+
<NInput
211+
ref="inputRef"
212+
v-model:value="debouncedSearch"
213+
class="search-input"
214+
:loading="loading"
215+
:disabled="loading"
216+
:placeholder="i18n('search', 'navbar')"
217+
autosize
218+
clearable
219+
:on-input-focus="() => toggleFocus(true)"
220+
:on-input-blur="() => toggleFocus(false)"
221+
>
222+
<template #prefix>
223+
<NIcon :component="IconLoop" />
224+
</template>
225+
</NInput>
226+
</NPopselect>
191227
</template>
192228
</NTooltip>
193229

src/stores/data/search.store.ts

+50-7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { type ListScrollItem, ListScrollItemType } from '~/models/list-scroll.mo
1010
import { NotificationService } from '~/services/notification.service';
1111
import { TraktService } from '~/services/trakt.service';
1212
import { storage } from '~/utils/browser/browser-storage.utils';
13+
import { debounce } from '~/utils/debounce.utils';
1314
import { debounceLoading, useLoadingPlaceholder } from '~/utils/store.utils';
1415

1516
export type SearchResult = Omit<TraktSearchResult, 'type'> & {
@@ -33,26 +34,64 @@ export const useSearchStore = defineStore('data.search', () => {
3334

3435
const searchResults = ref<SearchResult[]>([]);
3536

37+
const history = ref<Set<string>>(new Set());
38+
39+
const saveHistory = debounce(() => storage.local.set('data.search.history', [...history.value]), 1000);
40+
const saveSearch = debounce(() => storage.local.set('data.search.last', { value: search.value, date: Date.now() }), 1000);
41+
42+
const addToHistory = (value: string = search.value) => {
43+
const newArray = [...history.value, value].filter(Boolean);
44+
// Keep only the last 100 elements
45+
if (newArray.length > 100) {
46+
history.value = new Set(newArray.slice(-100));
47+
} else {
48+
history.value = new Set(newArray);
49+
}
50+
return Promise.all([saveHistory(), saveSearch()]);
51+
};
52+
3653
const clearState = () => {
3754
types.value = DefaultSearchType;
3855
query.value = false;
3956
pagination.value = undefined;
4057
search.value = '';
58+
history.value = new Set();
4159
};
4260

4361
const saveState = async () =>
4462
storage.local.set('data.search', {
4563
types: [...types.value],
4664
query: query.value,
65+
pageSize: pageSize.value,
66+
search: {
67+
value: search.value,
68+
date: new Date(),
69+
},
4770
});
4871

4972
const restoreState = async () => {
50-
const restored = await storage.local.get<{
51-
types: TraktSearchType[];
52-
query: boolean;
53-
}>('data.search');
54-
if (restored?.types) types.value = restored.types;
55-
if (restored?.query) query.value = restored.query;
73+
const [_state, _history, _search] = await Promise.all([
74+
storage.local.get<{
75+
types: TraktSearchType[];
76+
query: boolean;
77+
pageSize: number;
78+
}>('data.search'),
79+
storage.local.get<string[]>('data.search.history'),
80+
storage.local.get<{
81+
value: string;
82+
date: Date;
83+
}>('data.search.last'),
84+
]);
85+
86+
if (_state?.types) types.value = _state.types;
87+
if (_state?.query) query.value = _state.query;
88+
if (_state?.pageSize) pageSize.value = _state.pageSize;
89+
if (_history) history.value = new Set(_history);
90+
91+
if (_search?.date) {
92+
const day = new Date(_search.date).toLocaleDateString();
93+
if (day === new Date().toLocaleDateString()) search.value = _search.value;
94+
}
5695
};
5796

5897
const loadingPlaceholder = useLoadingPlaceholder<SearchResult>(pageSize);
@@ -105,7 +144,10 @@ export const useSearchStore = defineStore('data.search', () => {
105144
const initSearchStore = async () => {
106145
await restoreState();
107146

108-
watch(search, () => fetchSearchResults());
147+
watch(search, async () => {
148+
await fetchSearchResults();
149+
await addToHistory().catch(e => console.error('Failed to save search history', e));
150+
});
109151

110152
watch(pageSize, async () => {
111153
await fetchSearchResults();
@@ -122,6 +164,7 @@ export const useSearchStore = defineStore('data.search', () => {
122164
types,
123165
query,
124166
search,
167+
history,
125168
clearState,
126169
initSearchStore,
127170
fetchSearchResults,

src/utils/store.utils.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ export const watchUserChange = ({
107107
return { active, user };
108108
};
109109

110-
export const useDebouncedSearch = (search: Ref<string>, delay = 350) => {
110+
export const useDebouncedSearch = (search: Ref<string>, delay = 350, disabled?: Ref<boolean>) => {
111111
const debouncedSearch = ref(search.value);
112112

113113
watch(search, () => {
@@ -119,6 +119,7 @@ export const useDebouncedSearch = (search: Ref<string>, delay = 350) => {
119119
watch(
120120
debouncedSearch,
121121
debounce(() => {
122+
if (disabled?.value) return;
122123
search.value = debouncedSearch.value;
123124
}, delay),
124125
);

0 commit comments

Comments
 (0)