search/app/pages/conferencias.vue

197 lines
5.1 KiB
Vue
Executable File

<script setup lang="ts">
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
import { breakpointsTailwind, useDebounce } from '@vueuse/core'
import type { SearchHit } from '~/types'
import InboxActivity from '~/components/inbox/InboxActivity.vue'
const PAGE_SIZE = 15
const REQUEST_TIMEOUT_MS = 15000
const query = ref('')
const debouncedQuery = useDebounce(query, 150)
const loading = ref(false)
const loadingMore = ref(false)
const errorMsg = ref<string | null>(null)
const meili = useMeiliSearchRef()
const hits = ref<SearchHit[]>([])
const total = ref(0)
const hasMore = computed(() => hits.value.length < total.value)
let searchSeq = 0
let abortController: AbortController | null = null
async function runSearch(q: string, append = false) {
abortController?.abort()
const ac = new AbortController()
abortController = ac
const seq = ++searchSeq
if (append) loadingMore.value = true
else loading.value = true
errorMsg.value = null
const timeoutId = setTimeout(() => ac.abort(), REQUEST_TIMEOUT_MS)
try {
const res = await meili.index('conferences_ES').search(q || '', {
attributesToRetrieve: ['*'],
showMatchesPosition: true,
limit: PAGE_SIZE,
offset: append ? hits.value.length : 0,
sort: q ? undefined : ['isodate:desc']
}, { signal: ac.signal })
if (seq !== searchSeq) return
const newHits = (res?.hits ?? []) as SearchHit[]
hits.value = append ? hits.value.concat(newHits) : newHits
total.value = res?.estimatedTotalHits ?? hits.value.length
} catch (err: unknown) {
const name = (err as { name?: string })?.name
if (name === 'AbortError') return
if (seq !== searchSeq) return
console.error('Meilisearch error', err)
errorMsg.value = (err as Error)?.message || 'Error al buscar.'
if (!append) {
hits.value = []
total.value = 0
}
} finally {
clearTimeout(timeoutId)
if (seq === searchSeq) {
loading.value = false
loadingMore.value = false
}
}
}
function loadMore() {
if (loadingMore.value || loading.value || !hasMore.value) return
runSearch(query.value, true)
}
onMounted(() => runSearch(''))
onBeforeUnmount(() => {
abortController?.abort()
})
watch(debouncedQuery, (q) => {
hits.value = []
total.value = 0
runSearch(q, false)
})
const selected = ref<SearchHit | null>(null)
const isPanelOpen = computed({
get() { return !!selected.value },
set(value: boolean) { if (!value) selected.value = null }
})
watch(hits, () => {
if (!selected.value) return
const stillThere = hits.value.find(h => h._id === selected.value?._id)
if (!stillThere) selected.value = null
})
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = breakpoints.smaller('lg')
function retry() {
runSearch(query.value, false)
}
</script>
<template>
<UDashboardPanel
id="conferences-list"
:default-size="28"
:min-size="22"
:max-size="40"
resizable
>
<UDashboardNavbar title="Conferencias">
<template #leading>
<UDashboardSidebarCollapse />
</template>
<template #trailing>
<UBadge :label="total" variant="subtle" />
</template>
</UDashboardNavbar>
<div class="px-4 sm:px-6 py-3 border-b border-default">
<UInput
v-model="query"
icon="i-lucide-search"
placeholder='Buscar conferencias... (usa "comillas" para frase exacta)'
:loading="loading"
size="md"
class="w-full"
/>
<p class="mt-1.5 flex items-center gap-1 text-[11px] text-dimmed">
<UIcon name="i-lucide-lightbulb" class="size-3" />
<span>
Tip: envuelve en
<code class="px-1 rounded bg-elevated text-toned font-mono">"comillas"</code>
para frase exacta en ese orden.
</span>
</p>
</div>
<UAlert
v-if="errorMsg"
:title="errorMsg"
color="error"
variant="subtle"
icon="i-lucide-triangle-alert"
class="mx-4 my-2"
:actions="[{ label: 'Reintentar', color: 'neutral', variant: 'outline', onClick: retry }]"
/>
<InboxList
v-model="selected"
:activities="hits"
:query="debouncedQuery"
:has-more="hasMore"
:loading="loading"
:loading-more="loadingMore"
@load-more="loadMore"
/>
</UDashboardPanel>
<InboxActivity
v-if="selected"
:activity="selected"
collection="conferences"
:query="debouncedQuery"
@close="selected = null"
/>
<div v-else class="hidden lg:flex flex-1 items-center justify-center">
<div class="flex flex-col items-center gap-2 text-dimmed">
<UIcon name="i-lucide-search" class="size-16" />
<p class="text-sm">
{{ query ? 'Selecciona una coincidencia para ver el detalle' : 'Escribe arriba para buscar' }}
</p>
</div>
</div>
<ClientOnly>
<USlideover v-if="isMobile" v-model:open="isPanelOpen">
<template #content>
<InboxActivity
v-if="selected"
:activity="selected"
collection="conferences"
:query="debouncedQuery"
@close="selected = null"
/>
</template>
</USlideover>
</ClientOnly>
</template>