@meeovi/layer-search 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +181 -0
- package/app/components/README.md +3 -0
- package/app/components/atoms/BaseButton.vue +36 -0
- package/app/components/atoms/BaseCard.vue +13 -0
- package/app/components/atoms/BaseCheckbox.vue +51 -0
- package/app/components/atoms/BaseLogo.vue +19 -0
- package/app/components/atoms/BaseText.vue +17 -0
- package/app/components/atoms/BaseTitle.vue +23 -0
- package/app/components/atoms/DiscordIcon.vue +14 -0
- package/app/components/atoms/GithubIcon.vue +14 -0
- package/app/components/atoms/HalfSolidStarIcon.vue +5 -0
- package/app/components/atoms/SelectArrow.vue +5 -0
- package/app/components/atoms/SolidStarIcon.vue +5 -0
- package/app/components/atoms/StarIcon.vue +5 -0
- package/app/components/atoms/TwitterIcon.vue +14 -0
- package/app/components/atoms/WebIcon.vue +12 -0
- package/app/components/atoms/XIcon.vue +5 -0
- package/app/components/features/aiSearch.vue +0 -0
- package/app/components/features/allSearch.vue +0 -0
- package/app/components/features/autocomplete.vue +0 -0
- package/app/components/features/imageSearch.vue +0 -0
- package/app/components/features/videoSearch.vue +0 -0
- package/app/components/filters/filters.vue +0 -0
- package/app/components/molecules/BaseSelect.vue +53 -0
- package/app/components/molecules/PageNumber.vue +54 -0
- package/app/components/molecules/RangeSlider.vue +37 -0
- package/app/components/molecules/SearchInput.vue +32 -0
- package/app/components/molecules/SocialLink.vue +42 -0
- package/app/components/molecules/StarRating.vue +48 -0
- package/app/components/organisms/LoadingIndicator.vue +12 -0
- package/app/components/organisms/MeiliSearchBar.vue +15 -0
- package/app/components/organisms/MeiliSearchFacetFilter.vue +116 -0
- package/app/components/organisms/MeiliSearchLoadingProvider.vue +29 -0
- package/app/components/organisms/MeiliSearchPagination.vue +40 -0
- package/app/components/organisms/MeiliSearchProvider.vue +51 -0
- package/app/components/organisms/MeiliSearchRangeFilter.vue +52 -0
- package/app/components/organisms/MeiliSearchRatingFilter.vue +47 -0
- package/app/components/organisms/MeiliSearchResults.vue +35 -0
- package/app/components/organisms/MeiliSearchSorting.vue +23 -0
- package/app/components/organisms/MeiliSearchStats.vue +13 -0
- package/app/components/organisms/ProductCard.vue +80 -0
- package/app/components/organisms/TheNavbar.vue +71 -0
- package/app/components/results/audioSearch.vue +7 -0
- package/app/components/results/booksSearch.vue +7 -0
- package/app/components/results/financeSearch.vue +93 -0
- package/app/components/results/imageSearch.vue +7 -0
- package/app/components/results/musicSearch.vue +93 -0
- package/app/components/results/newsSearch.vue +93 -0
- package/app/components/results/spaceSearch.vue +93 -0
- package/app/components/results/spacesSearch.vue +7 -0
- package/app/components/results/travelSearch.vue +93 -0
- package/app/components/results/videoSearch.vue +7 -0
- package/app/components/search.vue +87 -0
- package/app/components/templates/HomeTemplate.vue +44 -0
- package/app/components/widgets/ClearRefinements.vue +27 -0
- package/app/components/widgets/NoResults.vue +125 -0
- package/app/components/widgets/PriceSlider.css +58 -0
- package/app/composables/adapter/meilisearch.ts +58 -0
- package/app/composables/adapter/mock.ts +34 -0
- package/app/composables/adapter/opensearch.ts +66 -0
- package/app/composables/adapter/types.ts +14 -0
- package/app/composables/bridges/instantsearch.ts +20 -0
- package/app/composables/bridges/react.ts +40 -0
- package/app/composables/bridges/vue.ts +38 -0
- package/app/composables/cli.ts +85 -0
- package/app/composables/config/schema.ts +16 -0
- package/app/composables/config.ts +20 -0
- package/app/composables/core/Facets.ts +9 -0
- package/app/composables/core/Filters.ts +13 -0
- package/app/composables/core/Normalizers.ts +0 -0
- package/app/composables/core/Pipeline.ts +20 -0
- package/app/composables/core/QueryBuilder.ts +27 -0
- package/app/composables/core/SearchContext.ts +54 -0
- package/app/composables/core/SearchManager.ts +27 -0
- package/app/composables/events.ts +6 -0
- package/app/composables/index.ts +9 -0
- package/app/composables/module.ts +72 -0
- package/app/composables/types/api/global-search.ts +8 -0
- package/app/composables/types.d.ts +12 -0
- package/app/composables/utils/normalizers.ts +6 -0
- package/app/pages/results.vue +85 -0
- package/app/plugins/instantsearch.js +35 -0
- package/app/plugins/search.js +20 -0
- package/nuxt.config.ts +11 -0
- package/package.json +43 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div v-if="results.length">
|
|
4
|
+
<v-row>
|
|
5
|
+
<v-col v-for="(result, index) in results" :key="index">
|
|
6
|
+
<productCard :product="result" />
|
|
7
|
+
</v-col>
|
|
8
|
+
</v-row>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<p v-else>No results found.</p>
|
|
12
|
+
|
|
13
|
+
<v-pagination v-model="currentPage" :length="totalPages" :total-visible="5"
|
|
14
|
+
@update:modelValue="fetchSearchResults"></v-pagination>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup>
|
|
19
|
+
import {
|
|
20
|
+
ref,
|
|
21
|
+
onMounted,
|
|
22
|
+
watch
|
|
23
|
+
} from 'vue';
|
|
24
|
+
import {
|
|
25
|
+
useRoute
|
|
26
|
+
} from 'vue-router';
|
|
27
|
+
import FilterComponent from '~/components/search/filters.vue';
|
|
28
|
+
import productCard from '~/components/commerce/commerce/product/productCard.vue';
|
|
29
|
+
import {
|
|
30
|
+
liteClient as algoliasearch
|
|
31
|
+
} from 'algoliasearch/lite';
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
const config = useRuntimeConfig();
|
|
35
|
+
const searchClient = algoliasearch(config.public.appId, config.public.apiKey);
|
|
36
|
+
|
|
37
|
+
const route = useRoute();
|
|
38
|
+
const results = ref([]);
|
|
39
|
+
const searchQuery = ref('');
|
|
40
|
+
const currentPage = ref(1);
|
|
41
|
+
const totalPages = ref(1);
|
|
42
|
+
const pageSize = ref(10);
|
|
43
|
+
const filterData = ref({});
|
|
44
|
+
const appliedFilters = ref({});
|
|
45
|
+
|
|
46
|
+
const fetchSearchResults = async () => {
|
|
47
|
+
if (route.query.q) {
|
|
48
|
+
searchQuery.value = route.query.q;
|
|
49
|
+
try {
|
|
50
|
+
const facetFilters = Object.entries(appliedFilters.value).map(([facet, values]) =>
|
|
51
|
+
values.map(value => `${facet}:${value}`)
|
|
52
|
+
).flat();
|
|
53
|
+
|
|
54
|
+
const response = await searchClient.search([{
|
|
55
|
+
indexName: config.public.indexName,
|
|
56
|
+
query: searchQuery.value,
|
|
57
|
+
page: currentPage.value - 1,
|
|
58
|
+
hitsPerPage: pageSize.value,
|
|
59
|
+
facets: ['categories'], // Adjust this based on your filterable attributes
|
|
60
|
+
facetFilters: facetFilters,
|
|
61
|
+
}, ]);
|
|
62
|
+
|
|
63
|
+
if (response.results && response.results[0]) {
|
|
64
|
+
results.value = response.results[0].hits;
|
|
65
|
+
totalPages.value = response.results[0].nbPages;
|
|
66
|
+
currentPage.value = response.results[0].page + 1;
|
|
67
|
+
updateFilters(response.results[0].facets);
|
|
68
|
+
} else {
|
|
69
|
+
results.value = [];
|
|
70
|
+
totalPages.value = 0;
|
|
71
|
+
currentPage.value = 1;
|
|
72
|
+
updateFilters({});
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('Search error:', err);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const updateFilters = (facets) => {
|
|
81
|
+
filterData.value = {
|
|
82
|
+
...facets
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleApplyFilters = (filters) => {
|
|
87
|
+
appliedFilters.value = filters;
|
|
88
|
+
fetchSearchResults();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
onMounted(fetchSearchResults);
|
|
92
|
+
watch(() => route.query.q, fetchSearchResults);
|
|
93
|
+
</script>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div v-if="results.length">
|
|
4
|
+
<v-row>
|
|
5
|
+
<v-col v-for="(result, index) in results" :key="index">
|
|
6
|
+
<productCard :product="result" />
|
|
7
|
+
</v-col>
|
|
8
|
+
</v-row>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<p v-else>No results found.</p>
|
|
12
|
+
|
|
13
|
+
<v-pagination v-model="currentPage" :length="totalPages" :total-visible="5"
|
|
14
|
+
@update:modelValue="fetchSearchResults"></v-pagination>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup>
|
|
19
|
+
import {
|
|
20
|
+
ref,
|
|
21
|
+
onMounted,
|
|
22
|
+
watch
|
|
23
|
+
} from 'vue';
|
|
24
|
+
import {
|
|
25
|
+
useRoute
|
|
26
|
+
} from 'vue-router';
|
|
27
|
+
import FilterComponent from '~/components/search/filters.vue';
|
|
28
|
+
import productCard from '~/components/commerce/commerce/product/productCard.vue';
|
|
29
|
+
import {
|
|
30
|
+
liteClient as algoliasearch
|
|
31
|
+
} from 'algoliasearch/lite';
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
const config = useRuntimeConfig();
|
|
35
|
+
const searchClient = algoliasearch(config.public.appId, config.public.apiKey);
|
|
36
|
+
|
|
37
|
+
const route = useRoute();
|
|
38
|
+
const results = ref([]);
|
|
39
|
+
const searchQuery = ref('');
|
|
40
|
+
const currentPage = ref(1);
|
|
41
|
+
const totalPages = ref(1);
|
|
42
|
+
const pageSize = ref(10);
|
|
43
|
+
const filterData = ref({});
|
|
44
|
+
const appliedFilters = ref({});
|
|
45
|
+
|
|
46
|
+
const fetchSearchResults = async () => {
|
|
47
|
+
if (route.query.q) {
|
|
48
|
+
searchQuery.value = route.query.q;
|
|
49
|
+
try {
|
|
50
|
+
const facetFilters = Object.entries(appliedFilters.value).map(([facet, values]) =>
|
|
51
|
+
values.map(value => `${facet}:${value}`)
|
|
52
|
+
).flat();
|
|
53
|
+
|
|
54
|
+
const response = await searchClient.search([{
|
|
55
|
+
indexName: config.public.indexName,
|
|
56
|
+
query: searchQuery.value,
|
|
57
|
+
page: currentPage.value - 1,
|
|
58
|
+
hitsPerPage: pageSize.value,
|
|
59
|
+
facets: ['categories'], // Adjust this based on your filterable attributes
|
|
60
|
+
facetFilters: facetFilters,
|
|
61
|
+
}, ]);
|
|
62
|
+
|
|
63
|
+
if (response.results && response.results[0]) {
|
|
64
|
+
results.value = response.results[0].hits;
|
|
65
|
+
totalPages.value = response.results[0].nbPages;
|
|
66
|
+
currentPage.value = response.results[0].page + 1;
|
|
67
|
+
updateFilters(response.results[0].facets);
|
|
68
|
+
} else {
|
|
69
|
+
results.value = [];
|
|
70
|
+
totalPages.value = 0;
|
|
71
|
+
currentPage.value = 1;
|
|
72
|
+
updateFilters({});
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('Search error:', err);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const updateFilters = (facets) => {
|
|
81
|
+
filterData.value = {
|
|
82
|
+
...facets
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleApplyFilters = (filters) => {
|
|
87
|
+
appliedFilters.value = filters;
|
|
88
|
+
fetchSearchResults();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
onMounted(fetchSearchResults);
|
|
92
|
+
watch(() => route.query.q, fetchSearchResults);
|
|
93
|
+
</script>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div v-if="results.length">
|
|
4
|
+
<v-row>
|
|
5
|
+
<v-col v-for="(result, index) in results" :key="index">
|
|
6
|
+
<productCard :product="result" />
|
|
7
|
+
</v-col>
|
|
8
|
+
</v-row>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<p v-else>No results found.</p>
|
|
12
|
+
|
|
13
|
+
<v-pagination v-model="currentPage" :length="totalPages" :total-visible="5"
|
|
14
|
+
@update:modelValue="fetchSearchResults"></v-pagination>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup>
|
|
19
|
+
import {
|
|
20
|
+
ref,
|
|
21
|
+
onMounted,
|
|
22
|
+
watch
|
|
23
|
+
} from 'vue';
|
|
24
|
+
import {
|
|
25
|
+
useRoute
|
|
26
|
+
} from 'vue-router';
|
|
27
|
+
import FilterComponent from '~/components/search/filters.vue';
|
|
28
|
+
import productCard from '~/components/commerce/commerce/product/productCard.vue';
|
|
29
|
+
import {
|
|
30
|
+
liteClient as algoliasearch
|
|
31
|
+
} from 'algoliasearch/lite';
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
const config = useRuntimeConfig();
|
|
35
|
+
const searchClient = algoliasearch(config.public.appId, config.public.apiKey);
|
|
36
|
+
|
|
37
|
+
const route = useRoute();
|
|
38
|
+
const results = ref([]);
|
|
39
|
+
const searchQuery = ref('');
|
|
40
|
+
const currentPage = ref(1);
|
|
41
|
+
const totalPages = ref(1);
|
|
42
|
+
const pageSize = ref(10);
|
|
43
|
+
const filterData = ref({});
|
|
44
|
+
const appliedFilters = ref({});
|
|
45
|
+
|
|
46
|
+
const fetchSearchResults = async () => {
|
|
47
|
+
if (route.query.q) {
|
|
48
|
+
searchQuery.value = route.query.q;
|
|
49
|
+
try {
|
|
50
|
+
const facetFilters = Object.entries(appliedFilters.value).map(([facet, values]) =>
|
|
51
|
+
values.map(value => `${facet}:${value}`)
|
|
52
|
+
).flat();
|
|
53
|
+
|
|
54
|
+
const response = await searchClient.search([{
|
|
55
|
+
indexName: config.public.indexName,
|
|
56
|
+
query: searchQuery.value,
|
|
57
|
+
page: currentPage.value - 1,
|
|
58
|
+
hitsPerPage: pageSize.value,
|
|
59
|
+
facets: ['categories'], // Adjust this based on your filterable attributes
|
|
60
|
+
facetFilters: facetFilters,
|
|
61
|
+
}, ]);
|
|
62
|
+
|
|
63
|
+
if (response.results && response.results[0]) {
|
|
64
|
+
results.value = response.results[0].hits;
|
|
65
|
+
totalPages.value = response.results[0].nbPages;
|
|
66
|
+
currentPage.value = response.results[0].page + 1;
|
|
67
|
+
updateFilters(response.results[0].facets);
|
|
68
|
+
} else {
|
|
69
|
+
results.value = [];
|
|
70
|
+
totalPages.value = 0;
|
|
71
|
+
currentPage.value = 1;
|
|
72
|
+
updateFilters({});
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('Search error:', err);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const updateFilters = (facets) => {
|
|
81
|
+
filterData.value = {
|
|
82
|
+
...facets
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleApplyFilters = (filters) => {
|
|
87
|
+
appliedFilters.value = filters;
|
|
88
|
+
fetchSearchResults();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
onMounted(fetchSearchResults);
|
|
92
|
+
watch(() => route.query.q, fetchSearchResults);
|
|
93
|
+
</script>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div v-if="results.length">
|
|
4
|
+
<v-row>
|
|
5
|
+
<v-col v-for="(result, index) in results" :key="index">
|
|
6
|
+
<productCard :product="result" />
|
|
7
|
+
</v-col>
|
|
8
|
+
</v-row>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<p v-else>No results found.</p>
|
|
12
|
+
|
|
13
|
+
<v-pagination v-model="currentPage" :length="totalPages" :total-visible="5"
|
|
14
|
+
@update:modelValue="fetchSearchResults"></v-pagination>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup>
|
|
19
|
+
import {
|
|
20
|
+
ref,
|
|
21
|
+
onMounted,
|
|
22
|
+
watch
|
|
23
|
+
} from 'vue';
|
|
24
|
+
import {
|
|
25
|
+
useRoute
|
|
26
|
+
} from 'vue-router';
|
|
27
|
+
import FilterComponent from '~/components/search/filters.vue';
|
|
28
|
+
import productCard from '~/components/commerce/commerce/product/productCard.vue';
|
|
29
|
+
import {
|
|
30
|
+
liteClient as algoliasearch
|
|
31
|
+
} from 'algoliasearch/lite';
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
const config = useRuntimeConfig();
|
|
35
|
+
const searchClient = algoliasearch(config.public.appId, config.public.apiKey);
|
|
36
|
+
|
|
37
|
+
const route = useRoute();
|
|
38
|
+
const results = ref([]);
|
|
39
|
+
const searchQuery = ref('');
|
|
40
|
+
const currentPage = ref(1);
|
|
41
|
+
const totalPages = ref(1);
|
|
42
|
+
const pageSize = ref(10);
|
|
43
|
+
const filterData = ref({});
|
|
44
|
+
const appliedFilters = ref({});
|
|
45
|
+
|
|
46
|
+
const fetchSearchResults = async () => {
|
|
47
|
+
if (route.query.q) {
|
|
48
|
+
searchQuery.value = route.query.q;
|
|
49
|
+
try {
|
|
50
|
+
const facetFilters = Object.entries(appliedFilters.value).map(([facet, values]) =>
|
|
51
|
+
values.map(value => `${facet}:${value}`)
|
|
52
|
+
).flat();
|
|
53
|
+
|
|
54
|
+
const response = await searchClient.search([{
|
|
55
|
+
indexName: config.public.indexName,
|
|
56
|
+
query: searchQuery.value,
|
|
57
|
+
page: currentPage.value - 1,
|
|
58
|
+
hitsPerPage: pageSize.value,
|
|
59
|
+
facets: ['categories'], // Adjust this based on your filterable attributes
|
|
60
|
+
facetFilters: facetFilters,
|
|
61
|
+
}, ]);
|
|
62
|
+
|
|
63
|
+
if (response.results && response.results[0]) {
|
|
64
|
+
results.value = response.results[0].hits;
|
|
65
|
+
totalPages.value = response.results[0].nbPages;
|
|
66
|
+
currentPage.value = response.results[0].page + 1;
|
|
67
|
+
updateFilters(response.results[0].facets);
|
|
68
|
+
} else {
|
|
69
|
+
results.value = [];
|
|
70
|
+
totalPages.value = 0;
|
|
71
|
+
currentPage.value = 1;
|
|
72
|
+
updateFilters({});
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('Search error:', err);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const updateFilters = (facets) => {
|
|
81
|
+
filterData.value = {
|
|
82
|
+
...facets
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleApplyFilters = (filters) => {
|
|
87
|
+
appliedFilters.value = filters;
|
|
88
|
+
fetchSearchResults();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
onMounted(fetchSearchResults);
|
|
92
|
+
watch(() => route.query.q, fetchSearchResults);
|
|
93
|
+
</script>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<div v-if="results.length">
|
|
4
|
+
<v-row>
|
|
5
|
+
<v-col v-for="(result, index) in results" :key="index">
|
|
6
|
+
<productCard :product="result" />
|
|
7
|
+
</v-col>
|
|
8
|
+
</v-row>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<p v-else>No results found.</p>
|
|
12
|
+
|
|
13
|
+
<v-pagination v-model="currentPage" :length="totalPages" :total-visible="5"
|
|
14
|
+
@update:modelValue="fetchSearchResults"></v-pagination>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup>
|
|
19
|
+
import {
|
|
20
|
+
ref,
|
|
21
|
+
onMounted,
|
|
22
|
+
watch
|
|
23
|
+
} from 'vue';
|
|
24
|
+
import {
|
|
25
|
+
useRoute
|
|
26
|
+
} from 'vue-router';
|
|
27
|
+
import FilterComponent from '~/components/search/filters.vue';
|
|
28
|
+
import productCard from '~/components/commerce/commerce/product/productCard.vue';
|
|
29
|
+
import {
|
|
30
|
+
liteClient as algoliasearch
|
|
31
|
+
} from 'algoliasearch/lite';
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
const config = useRuntimeConfig();
|
|
35
|
+
const searchClient = algoliasearch(config.public.appId, config.public.apiKey);
|
|
36
|
+
|
|
37
|
+
const route = useRoute();
|
|
38
|
+
const results = ref([]);
|
|
39
|
+
const searchQuery = ref('');
|
|
40
|
+
const currentPage = ref(1);
|
|
41
|
+
const totalPages = ref(1);
|
|
42
|
+
const pageSize = ref(10);
|
|
43
|
+
const filterData = ref({});
|
|
44
|
+
const appliedFilters = ref({});
|
|
45
|
+
|
|
46
|
+
const fetchSearchResults = async () => {
|
|
47
|
+
if (route.query.q) {
|
|
48
|
+
searchQuery.value = route.query.q;
|
|
49
|
+
try {
|
|
50
|
+
const facetFilters = Object.entries(appliedFilters.value).map(([facet, values]) =>
|
|
51
|
+
values.map(value => `${facet}:${value}`)
|
|
52
|
+
).flat();
|
|
53
|
+
|
|
54
|
+
const response = await searchClient.search([{
|
|
55
|
+
indexName: config.public.indexName,
|
|
56
|
+
query: searchQuery.value,
|
|
57
|
+
page: currentPage.value - 1,
|
|
58
|
+
hitsPerPage: pageSize.value,
|
|
59
|
+
facets: ['categories'], // Adjust this based on your filterable attributes
|
|
60
|
+
facetFilters: facetFilters,
|
|
61
|
+
}, ]);
|
|
62
|
+
|
|
63
|
+
if (response.results && response.results[0]) {
|
|
64
|
+
results.value = response.results[0].hits;
|
|
65
|
+
totalPages.value = response.results[0].nbPages;
|
|
66
|
+
currentPage.value = response.results[0].page + 1;
|
|
67
|
+
updateFilters(response.results[0].facets);
|
|
68
|
+
} else {
|
|
69
|
+
results.value = [];
|
|
70
|
+
totalPages.value = 0;
|
|
71
|
+
currentPage.value = 1;
|
|
72
|
+
updateFilters({});
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('Search error:', err);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const updateFilters = (facets) => {
|
|
81
|
+
filterData.value = {
|
|
82
|
+
...facets
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleApplyFilters = (filters) => {
|
|
87
|
+
appliedFilters.value = filters;
|
|
88
|
+
fetchSearchResults();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
onMounted(fetchSearchResults);
|
|
92
|
+
watch(() => route.query.q, fetchSearchResults);
|
|
93
|
+
</script>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="searchField">
|
|
3
|
+
<div class="container">
|
|
4
|
+
<ais-instant-search :search-client="searchClient" :index-name="indexName">
|
|
5
|
+
<ais-configure :hits-per-page.camel="8" />
|
|
6
|
+
<div class="search-panel">
|
|
7
|
+
<div class="search-panel__filters">
|
|
8
|
+
<ais-panel>
|
|
9
|
+
<template v-slot:header>type</template>
|
|
10
|
+
<ais-refinement-list attribute="type" />
|
|
11
|
+
</ais-panel>
|
|
12
|
+
|
|
13
|
+
<ais-panel>
|
|
14
|
+
<template v-slot:header>actors</template>
|
|
15
|
+
<ais-refinement-list searchable attribute="actors" />
|
|
16
|
+
</ais-panel>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<div class="search-panel__results">
|
|
20
|
+
<div class="searchbox">
|
|
21
|
+
<ais-search-box placeholder="" />
|
|
22
|
+
<v-text-field v-if="isDev" v-model="searchQuery" placeholder="Debug: search input" class="debug-search-input"></v-text-field>
|
|
23
|
+
</div>
|
|
24
|
+
<ais-hits>
|
|
25
|
+
<template v-slot:item="{ item, index }">
|
|
26
|
+
<article @click="openResult(item)" style="cursor:pointer">
|
|
27
|
+
<h1>
|
|
28
|
+
<ais-highlight attribute="title" :hit="item" />
|
|
29
|
+
</h1>
|
|
30
|
+
<p>
|
|
31
|
+
<ais-snippet :hit="item" attribute="plot" />
|
|
32
|
+
</p>
|
|
33
|
+
</article>
|
|
34
|
+
</template>
|
|
35
|
+
</ais-hits>
|
|
36
|
+
|
|
37
|
+
<div class="pagination">
|
|
38
|
+
<ais-pagination />
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</ais-instant-search>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script setup lang="ts">
|
|
48
|
+
import {
|
|
49
|
+
useRouter
|
|
50
|
+
} from 'vue-router'
|
|
51
|
+
|
|
52
|
+
import {
|
|
53
|
+
type Ref,
|
|
54
|
+
ref,
|
|
55
|
+
watch
|
|
56
|
+
} from 'vue';
|
|
57
|
+
import Client from '@searchkit/instantsearch-client'
|
|
58
|
+
|
|
59
|
+
const searchClient = Client({
|
|
60
|
+
url: '/api/search'
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const configDetails = useRuntimeConfig()
|
|
64
|
+
|
|
65
|
+
const router = useRouter()
|
|
66
|
+
const searchQuery = ref('');
|
|
67
|
+
const indexName = configDetails.public.indexName;
|
|
68
|
+
const isDev = process.env.NODE_ENV !== 'production'
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
console.debug('[search] searchClient', searchClient, 'has search method:', typeof searchClient.search)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function openResult(item: any) {
|
|
77
|
+
const id = item._id ?? item.id ?? '';
|
|
78
|
+
const title = item.title ?? '';
|
|
79
|
+
router.push({
|
|
80
|
+
path: '/results',
|
|
81
|
+
query: {
|
|
82
|
+
id,
|
|
83
|
+
title
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
const appConfig = useAppConfig()
|
|
3
|
+
|
|
4
|
+
const indexName = appConfig.ecommerce.indexName
|
|
5
|
+
|
|
6
|
+
const sortingOptions = [
|
|
7
|
+
{ value: `${indexName}`, label: 'Featured' },
|
|
8
|
+
{ value: `${indexName}:price:asc`, label: 'Price: Low to High' },
|
|
9
|
+
{ value: `${indexName}:price:desc`, label: 'Price: High to Low' },
|
|
10
|
+
{ value: `${indexName}:rating:desc`, label: 'Rating: High to Low' }
|
|
11
|
+
]
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<template>
|
|
15
|
+
<MeiliSearchProvider :index-name="indexName">
|
|
16
|
+
<TheNavbar class="mb-5 shadow-l">
|
|
17
|
+
<template #shared>
|
|
18
|
+
<MeiliSearchBar />
|
|
19
|
+
</template>
|
|
20
|
+
</TheNavbar>
|
|
21
|
+
<div class="container mb-5">
|
|
22
|
+
<div class="filters">
|
|
23
|
+
<MeiliSearchFacetFilter attribute="category" initial-sort-by="name" class="mb-5" />
|
|
24
|
+
<MeiliSearchFacetFilter attribute="brand" initial-sort-by="count" class="mb-5" />
|
|
25
|
+
<MeiliSearchRangeFilter attribute="price" class="mb-5" />
|
|
26
|
+
<MeiliSearchRatingFilter attribute="rating_rounded" label="Rating" />
|
|
27
|
+
</div>
|
|
28
|
+
<div class="results">
|
|
29
|
+
<div class="mb-5 results-meta">
|
|
30
|
+
<MeiliSearchStats />
|
|
31
|
+
<MeiliSearchSorting :options="sortingOptions" />
|
|
32
|
+
</div>
|
|
33
|
+
<MeiliSearchLoadingProvider v-slot="{ isSearchStalled }" class="mb-5">
|
|
34
|
+
<div v-show="isSearchStalled" style="height: 80vh; display: flex; flex-direction: column;">
|
|
35
|
+
<LoadingIndicator class="m-auto" />
|
|
36
|
+
</div>
|
|
37
|
+
<MeiliSearchResults v-show="!isSearchStalled" />
|
|
38
|
+
</MeiliSearchLoadingProvider>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</MeiliSearchProvider>
|
|
42
|
+
</template>
|
|
43
|
+
|
|
44
|
+
<style src="~/assets/css/components/home.css" scoped />
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="state" class="ais-ClearRefinements">
|
|
3
|
+
<button
|
|
4
|
+
type="reset"
|
|
5
|
+
class="ais-ClearRefinements-button"
|
|
6
|
+
@click.prevent="refine"
|
|
7
|
+
>
|
|
8
|
+
Reset filters
|
|
9
|
+
</button>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script>
|
|
14
|
+
import { connectClearRefinements } from 'instantsearch.js/es/connectors';
|
|
15
|
+
import { createWidgetMixin } from 'vue-instantsearch';
|
|
16
|
+
|
|
17
|
+
export default {
|
|
18
|
+
name: 'AppClearRefinements',
|
|
19
|
+
mixins: [createWidgetMixin({ connector: connectClearRefinements })],
|
|
20
|
+
methods: {
|
|
21
|
+
refine() {
|
|
22
|
+
this.state.refine();
|
|
23
|
+
this.$emit('click');
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
</script>
|