diff --git a/app/components/Header/SearchBox.vue b/app/components/Header/SearchBox.vue
index fe40f4696..4aeb910c9 100644
--- a/app/components/Header/SearchBox.vue
+++ b/app/components/Header/SearchBox.vue
@@ -1,7 +1,4 @@
@@ -10,7 +12,8 @@ defineProps<{
{{ keyword }}
diff --git a/app/composables/useGlobalSearch.ts b/app/composables/useGlobalSearch.ts
new file mode 100644
index 000000000..6ad738872
--- /dev/null
+++ b/app/composables/useGlobalSearch.ts
@@ -0,0 +1,75 @@
+import { normalizeSearchParam } from '#shared/utils/url'
+import { debounce } from 'perfect-debounce'
+
+// Pages that have their own local filter using ?q
+const pagesWithLocalFilter = new Set(['~username', 'org'])
+
+export function useGlobalSearch() {
+ const { searchProvider } = useSearchProvider()
+ const searchProviderValue = computed(() => {
+ const p = normalizeSearchParam(route.query.p)
+ if (p === 'npm' || searchProvider.value === 'npm') return 'npm'
+ return 'algolia'
+ })
+ const router = useRouter()
+ const route = useRoute()
+ const searchQuery = useState('search-query', () => {
+ if (pagesWithLocalFilter.has(route.name as string)) {
+ return ''
+ }
+ return normalizeSearchParam(route.query.q)
+ })
+
+ // clean search input when navigating away from search page
+ watch(
+ () => route.query.q,
+ urlQuery => {
+ const value = normalizeSearchParam(urlQuery)
+ if (!value) searchQuery.value = ''
+ },
+ )
+ const updateUrlQueryImpl = (value: string, provider: 'npm' | 'algolia') => {
+ const isSameQuery = route.query.q === value && route.query.p === provider
+ // Don't navigate away from pages that use ?q for local filtering
+ if (pagesWithLocalFilter.has(route.name as string) || isSameQuery) {
+ return
+ }
+
+ if (route.name === 'search') {
+ router.replace({
+ query: {
+ ...route.query,
+ q: value || undefined,
+ p: provider === 'npm' ? 'npm' : undefined,
+ },
+ })
+ return
+ }
+ router.push({
+ name: 'search',
+ query: {
+ q: value,
+ p: provider === 'npm' ? 'npm' : undefined,
+ },
+ })
+ }
+ const updateUrlQuery = debounce(updateUrlQueryImpl, 250)
+
+ function flushUpdateUrlQuery() {
+ updateUrlQuery.flush()
+ }
+
+ const searchQueryValue = computed({
+ get: () => searchQuery.value,
+ set: async (value: string) => {
+ searchQuery.value = value
+
+ // Leading debounce implementation as it doesn't work properly out of the box (https://github.com/unjs/perfect-debounce/issues/43)
+ if (!updateUrlQuery.isPending()) {
+ updateUrlQueryImpl(value, searchProvider.value)
+ }
+ updateUrlQuery(value, searchProvider.value)
+ },
+ })
+ return { model: searchQueryValue, provider: searchProviderValue, flushUpdateUrlQuery }
+}
diff --git a/app/composables/useGlobalSearchQuery.ts b/app/composables/useGlobalSearchQuery.ts
deleted file mode 100644
index 51f5b7d2d..000000000
--- a/app/composables/useGlobalSearchQuery.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { normalizeSearchParam } from '#shared/utils/url'
-
-export function useGlobalSearchQuery() {
- const route = useRoute()
- const searchQuery = useState('search-query', () => normalizeSearchParam(route.query.q))
-
- // clean search input when navigating away from search page
- watch(
- () => route.query.q,
- urlQuery => {
- const value = normalizeSearchParam(urlQuery)
- if (!value) searchQuery.value = ''
- },
- )
- return searchQuery
-}
diff --git a/app/composables/useStructuredFilters.ts b/app/composables/useStructuredFilters.ts
index 19266e1ee..a33d4d150 100644
--- a/app/composables/useStructuredFilters.ts
+++ b/app/composables/useStructuredFilters.ts
@@ -87,6 +87,7 @@ export function hasSearchOperators(parsed: ParsedSearchOperators): boolean {
interface UseStructuredFiltersOptions {
packages: Ref
+ searchQueryModel?: Ref
initialFilters?: Partial
initialSort?: SortOption
}
@@ -114,7 +115,7 @@ function matchesSecurity(pkg: NpmSearchResult, security: SecurityFilter): boolea
export function useStructuredFilters(options: UseStructuredFiltersOptions) {
const route = useRoute()
const router = useRouter()
- const { packages, initialFilters, initialSort } = options
+ const { packages, initialFilters, initialSort, searchQueryModel } = options
const { t } = useI18n()
const searchQuery = shallowRef(normalizeSearchParam(route.query.q))
@@ -404,6 +405,8 @@ export function useStructuredFilters(options: UseStructuredFiltersOptions) {
? `${searchQuery.value.trim()} keyword:${keyword}`
: `keyword:${keyword}`
router.replace({ query: { ...route.query, q: newQ } })
+
+ if (searchQueryModel) searchQueryModel.value = newQ
}
}
@@ -411,6 +414,7 @@ export function useStructuredFilters(options: UseStructuredFiltersOptions) {
filters.value.keywords = filters.value.keywords.filter(k => k !== keyword)
const newQ = searchQuery.value.replace(new RegExp(`keyword:${keyword}($| )`, 'g'), '').trim()
router.replace({ query: { ...route.query, q: newQ || undefined } })
+ if (searchQueryModel) searchQueryModel.value = newQ
}
function toggleKeyword(keyword: string) {
diff --git a/app/pages/index.vue b/app/pages/index.vue
index 35d01f08b..8e087b510 100644
--- a/app/pages/index.vue
+++ b/app/pages/index.vue
@@ -1,36 +1,11 @@