Skip to content
Closed
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 29 additions & 8 deletions app/components/AppHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,26 @@ const isMobile = useIsMobile()
const isSearchExpandedManually = shallowRef(false)
const searchBoxRef = useTemplateRef('searchBoxRef')

const searchQuery = shallowRef('')
watch(
() => route.query.q,
queryValue => {
searchQuery.value = normalizeSearchParam(queryValue)
},
{ immediate: true },
)

async function handleSearchSubmit() {
if (!searchQuery.value) {
return
}

await navigateTo({
name: 'search',
query: { q: searchQuery.value },
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the difference in nuxt between the router.push that was happening before, and this?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, I think either option would work.
Previously, this component had no route handling, so I copy-pasted the function from the homepage, as they serve the same purpose. I didn't check whether it was navigateTo or router.push, since they essentially perform the same action. I believe navigateTo is a helper function that can also be used on the server side.

}

// On search page, always show search expanded on mobile
const isOnHomePage = computed(() => route.name === 'index')
const isOnSearchPage = computed(() => route.name === 'search')
Expand Down Expand Up @@ -89,7 +109,7 @@ onKeyStroke(
<div class="absolute inset-0 bg-bg/80 backdrop-blur-md" />
<nav
:aria-label="$t('nav.main_navigation')"
class="relative container min-h-14 flex items-center gap-2 z-1"
class="relative container min-h-14 flex items-center gap-4 z-1"
:class="isOnHomePage ? 'justify-end' : 'justify-between'"
>
<!-- Mobile: Logo + search button (expands search, doesn't navigate) -->
Expand Down Expand Up @@ -120,18 +140,19 @@ onKeyStroke(
<span v-else class="hidden sm:block w-1" />

<!-- Center: Search bar + nav items -->
<div
class="flex-1 flex items-center justify-center md:gap-6"
:class="{ 'hidden sm:flex': !isSearchExpanded }"
>
<div class="flex-1 flex max-w-md md:gap-6" :class="{ 'hidden sm:flex': !isSearchExpanded }">
<!-- Search bar (hidden on mobile unless expanded) -->
<HeaderSearchBox
<SearchBox
v-if="!isOnHomePage"
ref="searchBoxRef"
:inputClass="isSearchExpanded ? 'w-full' : ''"
:class="{ 'max-w-md': !isSearchExpanded }"
class="max-w-sm"
compact
v-model="searchQuery"
@submit="handleSearchSubmit"
@focus="handleSearchFocus"
@blur="handleSearchBlur"
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.

<ul
v-if="!isSearchExpanded && isConnected && npmUser"
:class="{ hidden: showFullSearch }"
Expand Down
134 changes: 0 additions & 134 deletions app/components/Header/SearchBox.vue

This file was deleted.

95 changes: 95 additions & 0 deletions app/components/SearchBox.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<script setup lang="ts">
defineProps<{
compact?: boolean
}>()

const emit = defineEmits<{
(e: 'submit', searchQuery: string): void
(e: 'blur'): void
(e: 'focus'): void
}>()

const searchQuery = defineModel<string>({
default: '',
})
Comment thread
MatteoGabriele marked this conversation as resolved.

function handleSubmit() {
emit('submit', searchQuery.value)
}

function handleBlur() {
emit('blur')
}
function handleFocus() {
emit('focus')
}

// Expose focus method for parent components
const inputRef = useTemplateRef('inputRef')
function focus() {
inputRef.value?.focus()
}

defineExpose({
focus,
})
</script>

<template>
<search class="w-full @container">
<form method="GET" action="/search" class="relative" @submit.prevent="handleSubmit">
<label for="search-box" class="sr-only">
{{ $t('search.label') }}
Comment thread
ghostdevv marked this conversation as resolved.
</label>

<div class="relative group">
<div
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"
/>

<div class="search-box relative flex items-center">
<span
class="absolute text-fg-subtle font-mono 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"
:class="compact ? 'inset-is-3 text-sm' : 'inset-is-4 text-xl'"
>
/
</span>

<input
id="search-box"
ref="inputRef"
v-model.trim="searchQuery"
type="search"
name="q"
:placeholder="$t('search.placeholder')"
Comment thread
ghostdevv marked this conversation as resolved.
v-bind="noCorrect"
class="w-full bg-bg-subtle border border-border text-base 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)"
:class="
compact ? 'ps-7 pe-3 py-1.5 rounded-md text-sm!' : 'ps-8 pe-24 h-14 py-4 rounded-xl'
"
@blur="handleBlur"
@focus="handleFocus"
/>

<button
type="submit"
class="absolute hidden @xs:block group inset-ie-2.5 font-mono text-sm transition-[background-color,transform] duration-200 active:scale-95"
:class="
compact
? 'px-1.5 py-0.5 @md:ps-4 @md:pe-4'
: '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'
"
>
<span
class="inline-block i-carbon:search align-middle w-4 h-4 @md:me-2"
aria-hidden="true"
></span>
<span class="sr-only @md:not-sr-only">
{{ $t('search.button') }}
Comment thread
ghostdevv marked this conversation as resolved.
</span>
</button>
</div>
</div>
</form>
</search>
</template>
76 changes: 11 additions & 65 deletions app/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
<script setup lang="ts">
import { debounce } from 'perfect-debounce'
import { SHOWCASED_FRAMEWORKS } from '~/utils/frameworks'

const searchQuery = shallowRef('')
const searchInputRef = useTemplateRef('searchInputRef')
const { focused: isSearchFocused } = useFocus(searchInputRef)
async function handleSearchSubmit() {
if (!searchQuery.value) {
return
}

async function search() {
const query = searchQuery.value.trim()
if (!query) return
await navigateTo({
path: '/search',
query: query ? { q: query } : undefined,
name: 'search',
query: { q: searchQuery.value },
})
Comment on lines +5 to 13
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Trim whitespace-only queries before navigating.

Right now " " is treated as a valid query and will still navigate. Consider normalising to avoid empty/whitespace submissions (and mirror the same behaviour in the header submit handler for consistency).

Suggested tweak
 async function handleSearchSubmit() {
-  if (!searchQuery.value) {
+  const query = searchQuery.value.trim()
+  if (!query) {
     return
   }

   await navigateTo({
     name: 'search',
-    query: { q: searchQuery.value },
+    query: { q: query },
   })
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function handleSearchSubmit() {
if (!searchQuery.value) {
return
}
async function search() {
const query = searchQuery.value.trim()
if (!query) return
await navigateTo({
path: '/search',
query: query ? { q: query } : undefined,
name: 'search',
query: { q: searchQuery.value },
})
async function handleSearchSubmit() {
const query = searchQuery.value.trim()
if (!query) {
return
}
await navigateTo({
name: 'search',
query: { q: query },
})

const newQuery = searchQuery.value.trim()
if (newQuery !== query) {
await search()
}
}

const handleInput = isTouchDevice()
? search
: debounce(search, 250, { leading: true, trailing: true })

useSeoMeta({
title: () => $t('seo.home.title'),
ogTitle: () => $t('seo.home.title'),
Expand Down Expand Up @@ -62,56 +52,12 @@ defineOgImageComponent('Default', {
{{ $t('tagline') }}
</p>

<search
class="w-full max-w-xl motion-safe:animate-slide-up motion-safe:animate-fill-both"
<SearchBox
class="max-w-xl motion-safe:animate-slide-up motion-safe:animate-fill-both"
style="animation-delay: 0.2s"
>
<form method="GET" action="/search" class="relative" @submit.prevent.trim="search">
<label for="home-search" class="sr-only">
{{ $t('search.label') }}
</label>

<div class="relative group" :class="{ 'is-focused': isSearchFocused }">
<div
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"
/>

<div class="search-box relative flex items-center">
<span
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"
>
/
</span>

<input
id="home-search"
ref="searchInputRef"
v-model="searchQuery"
type="search"
name="q"
autofocus
:placeholder="$t('search.placeholder')"
v-bind="noCorrect"
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)"
@input="handleInput"
/>

<button
type="submit"
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"
>
<span
class="inline-block i-carbon:search align-middle w-4 h-4 sm:me-2"
aria-hidden="true"
></span>
<span class="sr-only sm:not-sr-only">
{{ $t('search.button') }}
</span>
</button>
</div>
</div>
</form>
</search>
v-model="searchQuery"
@submit="handleSearchSubmit"
/>

<BuildEnvironment class="mt-4" />
</header>
Expand Down
Loading
Loading