Skip to content

Commit 23393ae

Browse files
committed
feat: "I'm feeling lucky" search redirection with bang (!) ending
1 parent 7d69561 commit 23393ae

File tree

2 files changed

+46
-24
lines changed

2 files changed

+46
-24
lines changed

app/pages/search.vue

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const updateUrlPage = debounce((page: number) => {
3131
}, 500)
3232
3333
const { model: searchQuery, provider: searchProvider } = useGlobalSearch()
34-
const query = computed(() => searchQuery.value)
34+
const query = computed(() => searchQuery.value.trim().replace(/!$/, ''))
3535
3636
// Track if page just loaded (for hiding "Searching..." during view transition)
3737
const hasInteracted = shallowRef(false)
@@ -424,28 +424,34 @@ async function navigateToPackage(packageName: string) {
424424
// Track the input value when user pressed Enter (for navigating when results arrive)
425425
const pendingEnterQuery = shallowRef<string | null>(null)
426426
427-
// Watch for results to navigate when Enter was pressed before results arrived
428-
watch(displayResults, results => {
429-
if (!pendingEnterQuery.value) return
427+
// Watch for results to navigate when Enter was pressed before results arrived,
428+
// or for "I'm feeling lucky" redirection when the query ends with "!" and there is the exact match.
429+
watch(
430+
displayResults,
431+
results => {
432+
const rawQuery = normalizeSearchParam(route.query.q)
433+
const isFeelingLucky = rawQuery.endsWith('!')
430434
431-
// Check if input is still focused (user hasn't started navigating or clicked elsewhere)
432-
if (document.activeElement?.tagName !== 'INPUT') {
433-
pendingEnterQuery.value = null
434-
return
435-
}
435+
if (!pendingEnterQuery.value && !isFeelingLucky) return
436436
437-
// Navigate if first result matches the query that was entered
438-
const firstResult = results[0]
439-
// eslint-disable-next-line no-console
440-
console.log('[search] watcher fired', {
441-
pending: pendingEnterQuery.value,
442-
firstResult: firstResult?.package.name,
443-
})
444-
if (firstResult?.package.name === pendingEnterQuery.value) {
445-
pendingEnterQuery.value = null
446-
navigateToPackage(firstResult.package.name)
447-
}
448-
})
437+
// For manual Enter, check if input is still focused (user hasn't started navigating or clicked elsewhere)
438+
if (pendingEnterQuery.value && document.activeElement?.tagName !== 'INPUT') {
439+
pendingEnterQuery.value = null
440+
return
441+
}
442+
443+
const target = pendingEnterQuery.value || rawQuery.replace(/!$/, '')
444+
if (!target) return
445+
446+
// Navigate if first result matches the target query
447+
const firstResult = results[0]
448+
if (firstResult?.package.name === target) {
449+
pendingEnterQuery.value = null
450+
navigateToPackage(firstResult.package.name)
451+
}
452+
},
453+
{ immediate: true },
454+
)
449455
450456
function handleResultsKeydown(e: KeyboardEvent) {
451457
// If the active element is an input, navigate to exact match or wait for results
@@ -454,15 +460,17 @@ function handleResultsKeydown(e: KeyboardEvent) {
454460
const inputValue = (document.activeElement as HTMLInputElement).value.trim()
455461
if (!inputValue) return
456462
463+
const cleanedInputValue = inputValue.replace(/!$/, '')
464+
457465
// Check if first result matches the input value exactly
458466
const firstResult = displayResults.value[0]
459-
if (firstResult?.package.name === inputValue) {
467+
if (firstResult?.package.name === cleanedInputValue) {
460468
pendingEnterQuery.value = null
461469
return navigateToPackage(firstResult.package.name)
462470
}
463471
464-
// No match yet - store input value, watcher will handle navigation when results arrive
465-
pendingEnterQuery.value = inputValue
472+
// No match yet - store cleaned input value, watcher will handle navigation when results arrive
473+
pendingEnterQuery.value = cleanedInputValue
466474
return
467475
}
468476
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { expect, test } from './test-utils'
2+
3+
test.describe('Search "I\'m Feeling Lucky" Redirect', () => {
4+
test('direct URL access with "!" should redirect to package', async ({ page, goto }) => {
5+
await goto('/search?q=nuxt!', { waitUntil: 'hydration' })
6+
await expect(page).toHaveURL(/\/package\/nuxt/, { timeout: 15000 })
7+
})
8+
9+
test('normal search query (without "!") should not redirect', async ({ page, goto }) => {
10+
await goto('/search?q=nuxt', { waitUntil: 'hydration' })
11+
await expect(page.locator('[data-result-index="0"]').first()).toBeVisible({ timeout: 15000 })
12+
await expect(page).toHaveURL(/\/search\?q=nuxt/)
13+
})
14+
})

0 commit comments

Comments
 (0)