@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,54 @@
|
|
|
1
|
+
export interface SearchContextState {
|
|
2
|
+
query: string
|
|
3
|
+
page: number
|
|
4
|
+
pageSize: number
|
|
5
|
+
sort?: string
|
|
6
|
+
filters: Record<string, any>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class SearchContext {
|
|
10
|
+
state: SearchContextState
|
|
11
|
+
|
|
12
|
+
constructor(initial?: Partial<SearchContextState>) {
|
|
13
|
+
this.state = {
|
|
14
|
+
query: '',
|
|
15
|
+
page: 1,
|
|
16
|
+
pageSize: 20,
|
|
17
|
+
filters: {},
|
|
18
|
+
...initial
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
setQuery(query: string) {
|
|
23
|
+
this.state.query = query
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
setPage(page: number) {
|
|
27
|
+
this.state.page = page
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
setPageSize(size: number) {
|
|
31
|
+
this.state.pageSize = size
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setSort(sort: string | undefined) {
|
|
35
|
+
this.state.sort = sort
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setFilter(key: string, value: any) {
|
|
39
|
+
this.state.filters[key] = value
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
removeFilter(key: string) {
|
|
43
|
+
delete this.state.filters[key]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
reset() {
|
|
47
|
+
this.state = {
|
|
48
|
+
query: '',
|
|
49
|
+
page: 1,
|
|
50
|
+
pageSize: 20,
|
|
51
|
+
filters: {}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { SearchAdapter } from '@meeovi/core'
|
|
2
|
+
import { SearchContext, type SearchContextState } from './SearchContext'
|
|
3
|
+
import { QueryBuilder } from './QueryBuilder'
|
|
4
|
+
import { SearchPipeline } from './Pipeline'
|
|
5
|
+
|
|
6
|
+
export class SearchManager<TItem = any> {
|
|
7
|
+
context: SearchContext
|
|
8
|
+
pipeline: SearchPipeline
|
|
9
|
+
adapter: SearchAdapter<TItem>
|
|
10
|
+
|
|
11
|
+
constructor(adapter: SearchAdapter<TItem>, initial?: Partial<SearchContextState>) {
|
|
12
|
+
this.context = new SearchContext(initial)
|
|
13
|
+
this.pipeline = new SearchPipeline()
|
|
14
|
+
this.adapter = adapter
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async search() {
|
|
18
|
+
const builder = new QueryBuilder(this.context)
|
|
19
|
+
let query = builder.build()
|
|
20
|
+
|
|
21
|
+
query = this.pipeline.runBefore(query)
|
|
22
|
+
|
|
23
|
+
const result = await this.adapter.search(query as any)
|
|
24
|
+
|
|
25
|
+
return this.pipeline.runAfter(result)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from './module'
|
|
2
|
+
export * from './adapter/types'
|
|
3
|
+
export * from './core/SearchManager'
|
|
4
|
+
export * from './core/SearchContext'
|
|
5
|
+
export * from './core/Filters'
|
|
6
|
+
export * from './core/Facets'
|
|
7
|
+
export * from './bridges/instantsearch'
|
|
8
|
+
export * from './bridges/vue'
|
|
9
|
+
export * from './bridges/react'
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineAlternateModule,
|
|
3
|
+
useAlternateEventBus,
|
|
4
|
+
useAlternateContext,
|
|
5
|
+
type AlternateContext
|
|
6
|
+
} from '@meeovi/core'
|
|
7
|
+
|
|
8
|
+
import { validateSearchConfig, type SearchModuleConfig } from './config/schema'
|
|
9
|
+
import type { SearchAdapter } from '@meeovi/core'
|
|
10
|
+
import { createOpenSearchAdapter } from './adapter/opensearch'
|
|
11
|
+
import { createMeilisearchAdapter } from './adapter/meilisearch'
|
|
12
|
+
import { SearchManager } from './core/SearchManager'
|
|
13
|
+
import type { MeeoviSearchItem } from './adapter/types'
|
|
14
|
+
|
|
15
|
+
declare module '@meeovi/core' {
|
|
16
|
+
interface AlternateConfig {
|
|
17
|
+
search?: SearchModuleConfig
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface AlternateContext {
|
|
21
|
+
searchManager?: import('./core/SearchManager').SearchManager<MeeoviSearchItem>
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default defineAlternateModule({
|
|
26
|
+
id: 'search',
|
|
27
|
+
adapters: {},
|
|
28
|
+
|
|
29
|
+
async setup(ctx: AlternateContext) {
|
|
30
|
+
const bus = useAlternateEventBus()
|
|
31
|
+
const config = ctx.config.search
|
|
32
|
+
|
|
33
|
+
if (!config) return
|
|
34
|
+
|
|
35
|
+
validateSearchConfig(config)
|
|
36
|
+
|
|
37
|
+
if (config.defaultProvider === 'opensearch') {
|
|
38
|
+
const providerConfig = config.providers.opensearch as
|
|
39
|
+
| Parameters<typeof createOpenSearchAdapter>[0]
|
|
40
|
+
| undefined
|
|
41
|
+
|
|
42
|
+
if (providerConfig) {
|
|
43
|
+
this.adapters = {
|
|
44
|
+
search: createOpenSearchAdapter(providerConfig)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (config.defaultProvider === 'meilisearch') {
|
|
50
|
+
const providerConfig = config.providers.meilisearch as
|
|
51
|
+
| Parameters<typeof createMeilisearchAdapter>[0]
|
|
52
|
+
| undefined
|
|
53
|
+
|
|
54
|
+
if (providerConfig) {
|
|
55
|
+
this.adapters = {
|
|
56
|
+
search: createMeilisearchAdapter(providerConfig)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const adapter = ctx.getAdapter('search') as
|
|
62
|
+
| SearchAdapter<MeeoviSearchItem>
|
|
63
|
+
| undefined
|
|
64
|
+
if (adapter) {
|
|
65
|
+
ctx.searchManager = new SearchManager<MeeoviSearchItem>(adapter)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
bus.on('app:ready', () => {
|
|
69
|
+
console.info('[@meeovi/search] Search module initialized')
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
})
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
declare module '#imports' {
|
|
2
|
+
export function useRuntimeConfig(): { public?: { searchUrl?: string } }
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
declare module '@searchkit/client' {
|
|
6
|
+
export class SearchkitClient {
|
|
7
|
+
constructor(config?: { url?: string; host?: string })
|
|
8
|
+
query(...args: unknown[]): unknown
|
|
9
|
+
fetch(...args: unknown[]): unknown
|
|
10
|
+
}
|
|
11
|
+
export type SearchkitClientConfig = { url?: string; host?: string }
|
|
12
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="resultsPage">
|
|
3
|
+
<ais-instant-search :search-client="searchClient" :index-name="indexName">
|
|
4
|
+
<header class="search-header">
|
|
5
|
+
<ais-search-box />
|
|
6
|
+
<ais-stats />
|
|
7
|
+
<ais-sort-by :items="sortingOptions" />
|
|
8
|
+
</header>
|
|
9
|
+
|
|
10
|
+
<div class="search-body">
|
|
11
|
+
<aside class="filters">
|
|
12
|
+
<ais-refinement-list attribute="category" />
|
|
13
|
+
<ais-refinement-list attribute="brand" />
|
|
14
|
+
<ais-range-input attribute="price" />
|
|
15
|
+
<ais-rating-menu attribute="rating_rounded" />
|
|
16
|
+
</aside>
|
|
17
|
+
|
|
18
|
+
<main class="results">
|
|
19
|
+
<ais-hits>
|
|
20
|
+
<template #item="{ item }">
|
|
21
|
+
<article @click="openResult(item)" class="hit">
|
|
22
|
+
<h3><ais-highlight attribute="title" :hit="item" /></h3>
|
|
23
|
+
<p><ais-snippet attribute="plot" :hit="item" /></p>
|
|
24
|
+
</article>
|
|
25
|
+
</template>
|
|
26
|
+
</ais-hits>
|
|
27
|
+
|
|
28
|
+
<ais-pagination />
|
|
29
|
+
</main>
|
|
30
|
+
</div>
|
|
31
|
+
</ais-instant-search>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup>
|
|
36
|
+
import { ref } from 'vue'
|
|
37
|
+
import { useRouter, useRoute } from 'vue-router'
|
|
38
|
+
import { useRuntimeConfig } from '#imports'
|
|
39
|
+
import Client from '@searchkit/instantsearch-client'
|
|
40
|
+
import { getSearchClient, getIndexName } from '../utils/search/client'
|
|
41
|
+
|
|
42
|
+
import 'instantsearch.css/themes/satellite-min.css'
|
|
43
|
+
|
|
44
|
+
// initialize search client using existing helper (falls back safely)
|
|
45
|
+
let searchClient
|
|
46
|
+
let indexName = 'default'
|
|
47
|
+
try {
|
|
48
|
+
searchClient = getSearchClient()
|
|
49
|
+
indexName = getIndexName()
|
|
50
|
+
} catch (e) {
|
|
51
|
+
// fallback placeholder
|
|
52
|
+
// eslint-disable-next-line no-console
|
|
53
|
+
console.warn('Search client unavailable:', e.message || e)
|
|
54
|
+
searchClient = { _client: 'searchkit-fallback' }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const router = useRouter()
|
|
58
|
+
const route = useRoute()
|
|
59
|
+
|
|
60
|
+
function openResult(item) {
|
|
61
|
+
const id = item._id ?? item.id ?? ''
|
|
62
|
+
const title = item.title ?? item.name ?? ''
|
|
63
|
+
router.push({ path: route.path, query: { ...route.query, id, title } })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sortingOptions = [
|
|
67
|
+
{ value: indexName, label: 'Featured' },
|
|
68
|
+
{ value: `${indexName}:price:asc`, label: 'Price: Low to High' },
|
|
69
|
+
{ value: `${indexName}:price:desc`, label: 'Price: High to Low' },
|
|
70
|
+
{ value: `${indexName}:rating:desc`, label: 'Rating: High to Low' }
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
definePageMeta({ layout: 'nolive' })
|
|
74
|
+
useHead({ title: 'Search Results' })
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<style scoped>
|
|
78
|
+
.search-body {
|
|
79
|
+
display: flex;
|
|
80
|
+
gap: 1rem;
|
|
81
|
+
}
|
|
82
|
+
.filters { width: 260px }
|
|
83
|
+
.results { flex: 1 }
|
|
84
|
+
.hit { padding: 0.75rem; border-bottom: 1px solid #eee }
|
|
85
|
+
</style>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { h } from 'vue'
|
|
2
|
+
import { defineNuxtPlugin } from '#imports'
|
|
3
|
+
import InstantSearch from 'vue-instantsearch/vue3/es'
|
|
4
|
+
|
|
5
|
+
export default defineNuxtPlugin(nuxtApp => {
|
|
6
|
+
// Register the original InstantSearch plugin (registers other components)
|
|
7
|
+
nuxtApp.vueApp.use(InstantSearch)
|
|
8
|
+
// Capture the original registered component (if the plugin registered it)
|
|
9
|
+
const OriginalInstantSearch = nuxtApp.vueApp.component('ais-instant-search') || InstantSearch
|
|
10
|
+
|
|
11
|
+
// Override `ais-instant-search` with a small wrapper that sets
|
|
12
|
+
// future.preserveSharedStateOnUnmount = true by default to silence
|
|
13
|
+
// the deprecation warning and adopt the new behavior.
|
|
14
|
+
const AisInstantSearchWrapper = {
|
|
15
|
+
name: 'AisInstantSearch',
|
|
16
|
+
inheritAttrs: false,
|
|
17
|
+
setup(_, { attrs, slots }) {
|
|
18
|
+
const mergedAttrs = {
|
|
19
|
+
...attrs,
|
|
20
|
+
future: {
|
|
21
|
+
preserveSharedStateOnUnmount: true,
|
|
22
|
+
...(attrs.future || {})
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
26
|
+
// helpful debug when running locally
|
|
27
|
+
// eslint-disable-next-line no-console
|
|
28
|
+
console.debug('[instantsearch] merged future options', mergedAttrs.future)
|
|
29
|
+
}
|
|
30
|
+
return () => h(OriginalInstantSearch, mergedAttrs, slots)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
nuxtApp.vueApp.component('ais-instant-search', AisInstantSearchWrapper)
|
|
35
|
+
})
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { defineNuxtPlugin } from '#imports'
|
|
2
|
+
import { getSearchClient, getIndexName } from '../utils/search/client'
|
|
3
|
+
|
|
4
|
+
export default defineNuxtPlugin((nuxtApp) => {
|
|
5
|
+
let searchClient
|
|
6
|
+
let indexName
|
|
7
|
+
try {
|
|
8
|
+
searchClient = getSearchClient()
|
|
9
|
+
indexName = getIndexName()
|
|
10
|
+
} catch (e) {
|
|
11
|
+
// fallback placeholder to avoid build/runtime crash when not configured
|
|
12
|
+
// eslint-disable-next-line no-console
|
|
13
|
+
console.warn('Search client not configured:', e.message || e)
|
|
14
|
+
searchClient = { _client: 'searchkit-fallback' }
|
|
15
|
+
indexName = 'default'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
nuxtApp.provide('searchClient', searchClient)
|
|
19
|
+
nuxtApp.provide('searchIndexName', indexName)
|
|
20
|
+
})
|
package/nuxt.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@meeovi/layer-search",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Powerful search module for the Alternate Framework.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"module": "dist/index.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"meeovi",
|
|
17
|
+
"search",
|
|
18
|
+
"module",
|
|
19
|
+
"alternate-framework",
|
|
20
|
+
"vue",
|
|
21
|
+
"instantsearch"
|
|
22
|
+
],
|
|
23
|
+
"bin": {
|
|
24
|
+
"meeovi-search": "./dist/cli.js"
|
|
25
|
+
},
|
|
26
|
+
"author": "Meeovi",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"type": "module",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.json"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@meeovi/core": "^1.0.3",
|
|
34
|
+
"@types/react": "^19.2.9",
|
|
35
|
+
"react": "^19.2.3",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vue": "^3.2.47"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.0.10",
|
|
41
|
+
"nuxt": "^4.3.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"declaration": true,
|
|
4
|
+
"emitDeclarationOnly": false,
|
|
5
|
+
"outDir": "dist",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"target": "ESNext",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"jsx": "react-jsx",
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"noEmitOnError": false
|
|
13
|
+
}
|
|
14
|
+
}
|