Skip to content

Commit fc08134

Browse files
refactor: create a general search box
1 parent 6bbbd85 commit fc08134

File tree

4 files changed

+137
-205
lines changed

4 files changed

+137
-205
lines changed

app/components/AppHeader.vue

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,26 @@ const isMobile = useIsMobile()
2121
const isSearchExpandedManually = shallowRef(false)
2222
const searchBoxRef = useTemplateRef('searchBoxRef')
2323
24+
const searchQuery = shallowRef('')
25+
watch(
26+
() => route.query.q,
27+
queryValue => {
28+
searchQuery.value = normalizeSearchParam(queryValue)
29+
},
30+
{ immediate: true },
31+
)
32+
33+
async function handleSearchSubmit() {
34+
if (!searchQuery.value) {
35+
return
36+
}
37+
38+
await navigateTo({
39+
name: 'search',
40+
query: { q: searchQuery.value },
41+
})
42+
}
43+
2444
// On search page, always show search expanded on mobile
2545
const isOnHomePage = computed(() => route.name === 'index')
2646
const isOnSearchPage = computed(() => route.name === 'search')
@@ -88,7 +108,7 @@ onKeyStroke(
88108
<header class="sticky top-0 z-50 bg-bg/80 backdrop-blur-md border-b border-border">
89109
<nav
90110
:aria-label="$t('nav.main_navigation')"
91-
class="container min-h-14 flex items-center gap-2"
111+
class="container min-h-14 flex items-center gap-4"
92112
:class="isOnHomePage ? 'justify-end' : 'justify-between'"
93113
>
94114
<!-- Mobile: Logo + search button (expands search, doesn't navigate) -->
@@ -119,18 +139,20 @@ onKeyStroke(
119139
<span v-else class="hidden sm:block w-1" />
120140

121141
<!-- Center: Search bar + nav items -->
122-
<div
123-
class="flex-1 flex items-center justify-center md:gap-6"
124-
:class="{ 'hidden sm:flex': !isSearchExpanded }"
125-
>
142+
<div class="flex-1 flex max-w-md md:gap-6" :class="{ 'hidden sm:flex': !isSearchExpanded }">
126143
<!-- Search bar (hidden on mobile unless expanded) -->
127-
<HeaderSearchBox
144+
<SearchBox
145+
v-if="!isOnHomePage"
128146
ref="searchBoxRef"
129-
:inputClass="isSearchExpanded ? 'w-full' : ''"
147+
class="max-w-sm"
130148
:class="{ 'max-w-md': !isSearchExpanded }"
149+
compact
150+
v-model:search-query="searchQuery"
151+
@submit="handleSearchSubmit"
131152
@focus="handleSearchFocus"
132153
@blur="handleSearchBlur"
133154
/>
155+
134156
<ul
135157
v-if="!isSearchExpanded && isConnected && npmUser"
136158
:class="{ hidden: showFullSearch }"

app/components/Header/SearchBox.vue

Lines changed: 0 additions & 134 deletions
This file was deleted.

app/components/SearchBox.vue

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
searchQuery: string
4+
compact?: boolean
5+
}>()
6+
7+
const emit = defineEmits<{
8+
(e: 'submit', searchQuery: string): void
9+
(e: 'blur'): void
10+
(e: 'focus'): void
11+
}>()
12+
13+
const searchQuery = defineModel<string>('searchQuery', {
14+
default: '',
15+
})
16+
17+
function handleSubmit(): void {
18+
emit('submit', searchQuery.value)
19+
}
20+
21+
function handleBlur() {
22+
emit('blur')
23+
}
24+
function handleFocus() {
25+
emit('focus')
26+
}
27+
28+
// Expose focus method for parent components
29+
const inputRef = useTemplateRef('inputRef')
30+
function focus() {
31+
inputRef.value?.focus()
32+
}
33+
34+
defineExpose({
35+
focus,
36+
})
37+
</script>
38+
39+
<template>
40+
<search class="w-full @container">
41+
<form method="GET" action="/search" class="relative" @submit.prevent.trim="handleSubmit">
42+
<label for="home-search" class="sr-only">
43+
{{ $t('search.label') }}
44+
</label>
45+
46+
<div class="relative group">
47+
<div
48+
class="absolute -inset-px rounded-lg bg-gradient-to-r from-fg/0 via-fg/5 to-fg/0 opacity-0 transition-opacity duration-500 blur-sm group-[.is-focused]:opacity-100"
49+
/>
50+
51+
<div class="search-box relative flex items-center">
52+
<span
53+
class="absolute inset-is-4 text-fg-subtle font-mono text-lg pointer-events-none transition-colors duration-200 motion-reduce:transition-none [.group:hover:not(:focus-within)_&]:text-fg/80 group-focus-within:text-accent z-1"
54+
>
55+
/
56+
</span>
57+
58+
<input
59+
id="home-search"
60+
ref="inputRef"
61+
v-model.trim="searchQuery"
62+
type="search"
63+
name="q"
64+
autofocus
65+
:placeholder="$t('search.placeholder')"
66+
v-bind="noCorrect"
67+
class="w-full bg-bg-subtle border border-border font-mono text-fg placeholder:text-fg-subtle transition-[border-color,outline-color] duration-300 motion-reduce:transition-none hover:border-fg-subtle outline-2 outline-transparent focus:border-accent focus-visible:(outline-2 outline-accent/70)"
68+
:class="
69+
compact
70+
? 'ps-7 pe-3 py-1.5 rounded-md text-sm'
71+
: 'ps-8 pe-24 h-14 py-4 rounded-xl text-base'
72+
"
73+
@blur="handleBlur"
74+
@focus="handleFocus"
75+
/>
76+
77+
<button
78+
type="submit"
79+
class="absolute hidden @xs:block group inset-ie-2.5 font-mono text-sm transition-[background-color,transform] duration-200 active:scale-95 focus-visible:outline-accent/70"
80+
:class="
81+
compact
82+
? 'px-1.5 py-0.5 @md:ps-4 @md:pe-4'
83+
: 'rounded-md px-2.5 @md:ps-4 @md:pe-4 py-2 text-bg bg-fg/90 hover:bg-fg! group-focus-within:bg-fg/80'
84+
"
85+
>
86+
<span
87+
class="inline-block i-carbon:search align-middle w-4 h-4 @md:me-2"
88+
aria-hidden="true"
89+
></span>
90+
<span class="sr-only @md:not-sr-only">
91+
{{ $t('search.button') }}
92+
</span>
93+
</button>
94+
</div>
95+
</div>
96+
</form>
97+
</search>
98+
</template>

app/pages/index.vue

Lines changed: 10 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,18 @@
11
<script setup lang="ts">
2-
import { debounce } from 'perfect-debounce'
32
import { SHOWCASED_FRAMEWORKS } from '~/utils/frameworks'
43
54
const searchQuery = shallowRef('')
6-
const searchInputRef = useTemplateRef('searchInputRef')
7-
const { focused: isSearchFocused } = useFocus(searchInputRef)
5+
async function handleSearchSubmit() {
6+
if (!searchQuery.value) {
7+
return
8+
}
89
9-
async function search() {
10-
const query = searchQuery.value.trim()
11-
if (!query) return
1210
await navigateTo({
13-
path: '/search',
14-
query: query ? { q: query } : undefined,
11+
name: 'search',
12+
query: { q: searchQuery.value },
1513
})
16-
const newQuery = searchQuery.value.trim()
17-
if (newQuery !== query) {
18-
await search()
19-
}
2014
}
2115
22-
const handleInput = isTouchDevice()
23-
? search
24-
: debounce(search, 250, { leading: true, trailing: true })
25-
2616
useSeoMeta({
2717
title: () => $t('seo.home.title'),
2818
ogTitle: () => $t('seo.home.title'),
@@ -62,56 +52,12 @@ defineOgImageComponent('Default', {
6252
{{ $t('tagline') }}
6353
</p>
6454

65-
<search
55+
<SearchBox
6656
class="w-full max-w-xl motion-safe:animate-slide-up motion-safe:animate-fill-both"
6757
style="animation-delay: 0.2s"
68-
>
69-
<form method="GET" action="/search" class="relative" @submit.prevent.trim="search">
70-
<label for="home-search" class="sr-only">
71-
{{ $t('search.label') }}
72-
</label>
73-
74-
<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
75-
<div
76-
class="absolute -inset-px rounded-lg bg-gradient-to-r from-fg/0 via-fg/5 to-fg/0 opacity-0 transition-opacity duration-500 blur-sm group-[.is-focused]:opacity-100"
77-
/>
78-
79-
<div class="search-box relative flex items-center">
80-
<span
81-
class="absolute inset-is-4 text-fg-subtle font-mono text-lg pointer-events-none transition-colors duration-200 motion-reduce:transition-none [.group:hover:not(:focus-within)_&]:text-fg/80 group-focus-within:text-accent z-1"
82-
>
83-
/
84-
</span>
85-
86-
<input
87-
id="home-search"
88-
ref="searchInputRef"
89-
v-model="searchQuery"
90-
type="search"
91-
name="q"
92-
autofocus
93-
:placeholder="$t('search.placeholder')"
94-
v-bind="noCorrect"
95-
class="w-full bg-bg-subtle border border-border rounded-xl ps-8 pe-24 h-14 py-4 font-mono text-base text-fg placeholder:text-fg-subtle transition-[border-color,outline-color] duration-300 motion-reduce:transition-none hover:border-fg-subtle outline-2 outline-transparent focus:border-accent focus-visible:(outline-2 outline-accent/70)"
96-
@input="handleInput"
97-
/>
98-
99-
<button
100-
type="submit"
101-
class="absolute group inset-ie-2.5 px-2.5 sm:ps-4 sm:pe-4 py-2 font-mono text-sm text-bg bg-fg/90 rounded-md transition-[background-color,transform] duration-200 hover:bg-fg! group-focus-within:bg-fg/80 active:scale-95 focus-visible:outline-accent/70"
102-
>
103-
<span
104-
class="inline-block i-carbon:search align-middle w-4 h-4 sm:me-2"
105-
aria-hidden="true"
106-
></span>
107-
<span class="sr-only sm:not-sr-only">
108-
{{ $t('search.button') }}
109-
</span>
110-
</button>
111-
</div>
112-
</div>
113-
</form>
114-
</search>
58+
v-model:search-query="searchQuery"
59+
@submit="handleSearchSubmit"
60+
/>
11561

11662
<BuildEnvironment class="mt-4" />
11763
</header>

0 commit comments

Comments
 (0)