history / sub-pantalla studies / fix bugs title entrelinea

This commit is contained in:
David Ascanio 2026-05-14 11:52:08 -03:00
parent b22c58cc5a
commit e331de5f66
6 changed files with 381 additions and 179 deletions

View File

@ -1,71 +1,69 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, watch } from 'vue' import { computed, ref, watch } from 'vue'
import { useFavoritesStore } from '~/stores/favorites' import { useFavoritesStore } from '~/stores/favorites'
import { useHistoryStore } from '~/stores/history' import { useHistoryStore } from '~/stores/history'
import type { SearchHit } from '~/types' import type { SearchHit } from '~/types'
interface Study {
id?: number
title?: string
date?: string
place?: string
link?: string
}
interface EntrelineaDoc { interface EntrelineaDoc {
id?: string id?: string
image?: string image?: string
link?: string link?: string
locale?: string locale?: string
origin?: string
page?: number | string page?: number | string
text?: string text?: string
studies?: Study[]
[key: string]: unknown [key: string]: unknown
} }
const props = defineProps<{ const props = defineProps<{
document: EntrelineaDoc document: EntrelineaDoc
/** Identificador de la colección dentro del store de favoritos.
* Si no se pasa, no se renderiza el botón de favorito. */
collection?: string collection?: string
/** Versión del campo `text` con las coincidencias ya envueltas en
* <mark class="search-match"> por Typesense. Si viene, se usa para
* mostrar el cuerpo con resaltado; si no, cae al `document.text`. */
highlightedText?: string | null highlightedText?: string | null
}>() }>()
const emits = defineEmits<{ close: [] }>() const emits = defineEmits<{ close: [] }>()
/** URL de ImageKit con preset `detail` (1800px, q90). La construcción const showStudies = ref(false)
* centralizada vive en `~/utils/entrelineaImage` para no duplicarla. */
const imageUrl = computed<string | null>(() => {
return getEntrelineaImageUrl(props.document?.image, 'detail')
})
const title = computed(() => { // Volver al detalle cuando cambia el documento
return (props.document?.id as string) || 'Entrelínea' watch(() => props.document?.id, () => { showStudies.value = false })
})
const bodyHtml = computed<string>(() => { const imageUrl = computed<string | null>(() =>
return props.highlightedText || props.document?.text || '' getEntrelineaImageUrl(props.document?.image, 'detail')
}) )
const origin = computed(() => (props.document?.origin as string) || '')
const title = computed(() =>
origin.value || (props.document?.id as string) || 'Entrelínea'
)
const bodyHtml = computed<string>(() =>
props.highlightedText || props.document?.text || ''
)
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Favoritos */ /* Favoritos / Historial */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
const favorites = useFavoritesStore() const favorites = useFavoritesStore()
const history = useHistoryStore() const history = useHistoryStore()
const toast = useToast() const toast = useToast()
/** Adapta el documento de Typesense al shape SearchHit que pide el store
* de favoritos/historial (que se reusa entre todos los buscadores). */
function toSearchHit(doc: EntrelineaDoc): SearchHit { function toSearchHit(doc: EntrelineaDoc): SearchHit {
const id = doc.id || '' const id = doc.id || ''
return { return { _id: id, id, title: id || 'Entrelínea', body: doc.text, ...doc }
_id: id,
id,
title: id || 'Entrelínea',
body: doc.text,
...doc
}
} }
// Registrar la visita en el historial cada vez que cambia el documento mostrado.
// El store deduplica por (collection, id): si el usuario reabre la misma
// entrelínea, el item salta arriba y se actualiza visitedAt. No registramos
// si no nos pasaron `collection` o si el documento aún no tiene id.
watch( watch(
() => [props.collection, props.document?.id] as const, () => [props.collection, props.document?.id] as const,
([collection, id]) => { ([collection, id]) => {
@ -96,10 +94,28 @@ function onToggleFavorite() {
<template> <template>
<UDashboardPanel id="entrelinea-detail"> <UDashboardPanel id="entrelinea-detail">
<UDashboardNavbar :title="title" :toggle="false"> <UDashboardNavbar
:toggle="false"
:ui="{
root: 'h-auto min-h-(--ui-header-height) py-2 items-start sm:items-center',
left: 'flex-1 min-w-0 py-1',
title: 'flex items-center gap-1.5 font-semibold text-highlighted min-w-0 whitespace-normal'
}"
>
<template #leading> <template #leading>
<!-- En sub-pantalla de estudios: volver al detalle -->
<UButton <UButton
icon="i-lucide-x" v-if="showStudies"
icon="i-lucide-arrow-left"
color="neutral"
variant="ghost"
class="-ms-1.5"
@click="showStudies = false"
/>
<!-- En detalle: cerrar el panel -->
<UButton
v-else
icon="i-lucide-arrow-left"
color="neutral" color="neutral"
variant="ghost" variant="ghost"
class="-ms-1.5" class="-ms-1.5"
@ -107,96 +123,192 @@ function onToggleFavorite() {
/> />
</template> </template>
<template #title>
<span
v-if="showStudies"
class="font-semibold text-sm block"
>
Estudios relacionados
<span class="text-muted font-normal">({{ document.studies?.length }})</span>
</span>
<span
v-else
class="font-semibold text-sm min-w-0 max-w-full block whitespace-normal leading-snug pr-1 [overflow-wrap:anywhere]"
:title="title"
>
{{ title }}
</span>
</template>
<template #right> <template #right>
<UButton <template v-if="!showStudies">
v-if="document.link" <UTooltip
:to="document.link" v-if="document.studies?.length"
target="_blank" :text="`Estudios relacionados (${document.studies.length})`"
icon="i-lucide-external-link" >
label="Ver en sitio" <UChip
color="primary" :text="document.studies.length"
variant="solid" color="primary"
size="sm" size="sm"
/> :show="!!document.studies?.length"
>
<UButton
icon="i-lucide-book-open"
color="neutral"
variant="ghost"
aria-label="Estudios relacionados"
@click="showStudies = true"
/>
</UChip>
</UTooltip>
<UTooltip v-if="collection" :text="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'">
<UButton
:icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
:color="isFav ? 'primary' : 'neutral'"
variant="ghost"
:aria-label="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'"
@click="onToggleFavorite"
/>
</UTooltip>
</template>
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<div class="flex flex-col sm:flex-row justify-between gap-2 p-4 sm:px-6 border-b border-default"> <div
<div class="min-w-0 flex items-center gap-2 text-xs text-muted"> v-if="!showStudies"
class="flex items-start justify-between gap-3 px-4 sm:px-6 py-2.5 border-b border-default bg-elevated/40 min-w-0 w-full"
>
<div class="flex flex-wrap items-center gap-2 min-w-0 flex-1">
<UBadge <UBadge
v-if="document.locale" v-if="document.locale"
:label="String(document.locale).toUpperCase()" :label="String(document.locale).toUpperCase()"
color="neutral" color="neutral"
variant="subtle" variant="subtle"
size="xs" size="xs"
class="shrink-0"
/> />
<span v-if="document.page != null">Página {{ document.page }}</span> <span
v-if="origin && origin !== document.id"
class="inline-flex items-start rounded-md bg-primary/10 text-primary text-xs font-medium px-2 py-0.5 min-w-0 max-w-full whitespace-normal leading-snug [overflow-wrap:anywhere]"
:title="origin"
>
{{ origin }}
</span>
<span v-if="document.page != null" class="text-xs text-muted flex items-center gap-1 shrink-0">
<UIcon name="i-lucide-file-text" class="size-3" />
Pág. {{ document.page }}
</span>
<span
v-if="document.id && document.id !== origin"
class="text-xs text-dimmed font-mono truncate max-w-[160px] shrink-0"
:title="String(document.id)"
>
{{ document.id }}
</span>
</div> </div>
<UButton
v-if="document.link"
:to="document.link"
target="_blank"
icon="i-lucide-external-link"
label="Ver en sitio"
color="primary"
variant="soft"
size="xs"
class="shrink-0"
/>
</div> </div>
<div class="flex-1 overflow-y-auto relative"> <!-- ------------------------------------------------------------------ -->
<!-- <!-- VISTA PRINCIPAL: imagen + texto + accesos -->
Padding extra a la derecha (pe-16/20) y al fondo (pb-24) para que el <!-- ------------------------------------------------------------------ -->
FAB sticky de favorito NUNCA se superponga al contenido: el contenedor <div v-if="!showStudies" class="flex-1 overflow-y-auto">
reserva una "zona muerta" en la esquina inferior derecha donde el <div class="p-4 sm:p-6 pb-10 flex flex-col gap-6">
botón puede flotar sin tapar texto ni la imagen.
--> <!-- Imagen -->
<div class="p-4 sm:p-6 pe-16 sm:pe-20 pb-24 flex flex-col gap-6">
<!-- Imagen desde ImageKit -->
<div <div
v-if="imageUrl" v-if="imageUrl"
class="rounded-lg overflow-hidden border border-default bg-elevated flex items-center justify-center" class="rounded-xl overflow-hidden border border-default shadow-sm bg-elevated flex items-center justify-center"
> >
<img <img :src="imageUrl" :alt="title" loading="lazy" class="max-w-full h-auto">
:src="imageUrl"
:alt="title"
loading="lazy"
class="max-w-full h-auto"
>
</div> </div>
<div <div
v-else v-else
class="rounded-lg border border-dashed border-default p-8 text-center text-xs text-dimmed" class="rounded-xl border border-dashed border-default p-8 text-center text-xs text-dimmed"
> >
<UIcon name="i-lucide-image-off" class="size-6 mb-1 mx-auto" /> <UIcon name="i-lucide-image-off" class="size-6 mb-1 mx-auto" />
<p>Sin imagen disponible</p> <p>Sin imagen disponible</p>
</div> </div>
<!-- Entrelínea (text) usa la versión resaltada si Typesense la <!-- Texto -->
devolvió, si no el HTML crudo del documento. --> <div v-if="bodyHtml" class="rounded-xl border border-default bg-white dark:bg-neutral-900 shadow-sm p-4 sm:p-6">
<article <article
v-if="bodyHtml" class="prose prose-sm max-w-none dark:prose-invert leading-relaxed"
class="prose prose-sm max-w-none dark:prose-invert" v-html="bodyHtml"
v-html="bodyHtml" />
/> </div>
<p v-else class="text-sm text-muted"> <p v-else class="text-sm text-muted">
No hay entrelínea para esta coincidencia. No hay entrelínea para esta coincidencia.
</p> </p>
</div>
<!-- <!-- Link al sitio -->
FAB de favorito mismo patrón que en InboxActivity: contenedor <a
`sticky bottom-0 h-0` con `pointer-events-none` para que ocupe cero v-if="document.link"
altura en el flujo y el botón quede anclado al borde inferior :href="document.link"
visible mientras el usuario hace scroll. El padding `pe-16/pb-24` target="_blank"
del contenedor de contenido garantiza que NUNCA tape texto ni imagen. rel="noopener noreferrer"
--> class="flex items-center gap-3 rounded-xl border border-primary/30 bg-primary/5 hover:bg-primary/10 transition-colors px-4 py-3 group"
<div >
v-if="collection" <UIcon name="i-lucide-external-link" class="size-5 text-primary shrink-0" />
class="sticky bottom-0 inset-x-0 h-0 z-20 pointer-events-none" <div class="min-w-0 flex-1">
> <p class="text-sm font-medium text-primary">Ver en el sitio</p>
<UTooltip :text="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'"> <p class="text-xs text-muted truncate">{{ document.link }}</p>
<UButton </div>
:icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'" <UIcon name="i-lucide-chevron-right" class="size-4 text-primary/60 shrink-0 group-hover:translate-x-0.5 transition-transform" />
:color="isFav ? 'primary' : 'neutral'" </a>
variant="solid"
size="xl"
:aria-label="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'"
class="absolute bottom-4 end-4 sm:bottom-6 sm:end-6 rounded-full shadow-lg shadow-black/15 dark:shadow-black/40 ring-1 ring-default pointer-events-auto transition-transform hover:scale-105 active:scale-95"
@click="onToggleFavorite"
/>
</UTooltip>
</div> </div>
</div> </div>
<!-- ------------------------------------------------------------------ -->
<!-- SUB-PANTALLA: lista de estudios relacionados -->
<!-- ------------------------------------------------------------------ -->
<div v-else class="flex-1 overflow-y-auto divide-y divide-default">
<a
v-for="study in document.studies"
:key="study.id"
:href="study.link"
target="_blank"
rel="noopener noreferrer"
:title="study.title"
class="flex items-center gap-3 px-4 sm:px-6 py-3.5 hover:bg-primary/5 transition-colors group"
>
<div class="flex flex-col justify-center shrink-0 w-14 text-center">
<template v-if="study.date">
<span class="text-base font-bold text-highlighted tabular-nums leading-none">
{{ new Date(study.date + 'T00:00:00').getUTCDate() }}
</span>
<span class="text-[10px] uppercase tracking-wide text-muted leading-tight mt-0.5">
{{ new Date(study.date + 'T00:00:00').toLocaleDateString('es', { month: 'short', timeZone: 'UTC' }) }}
{{ new Date(study.date + 'T00:00:00').getUTCFullYear() }}
</span>
</template>
<UIcon v-else name="i-lucide-calendar-x" class="size-4 text-dimmed mx-auto" />
</div>
<div class="w-px self-stretch bg-default shrink-0" />
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-highlighted leading-snug group-hover:text-primary transition-colors line-clamp-2">
{{ study.title }}
</p>
<p v-if="study.place" class="text-xs text-muted truncate mt-0.5">
{{ study.place }}
</p>
</div>
<UIcon name="i-lucide-arrow-up-right" class="size-4 text-muted shrink-0 group-hover:text-primary transition-colors" />
</a>
</div>
</UDashboardPanel> </UDashboardPanel>
</template> </template>

View File

@ -369,10 +369,10 @@ function onInputKey(e: KeyboardEvent) {
<template> <template>
<UDashboardPanel id="activity-detail"> <UDashboardPanel id="activity-detail">
<UDashboardNavbar :title="activity.title" :toggle="false"> <UDashboardNavbar :toggle="false">
<template #leading> <template #leading>
<UButton <UButton
icon="i-lucide-x" icon="i-lucide-arrow-left"
color="neutral" color="neutral"
variant="ghost" variant="ghost"
class="-ms-1.5" class="-ms-1.5"
@ -380,11 +380,14 @@ function onInputKey(e: KeyboardEvent) {
/> />
</template> </template>
<template #title>
<span class="truncate font-semibold text-sm min-w-0 block" :title="activity.title">
{{ activity.title }}
</span>
</template>
<template #right> <template #right>
<!-- El botón de favorito ya no vive aquí: ahora es un FAB flotante <UTooltip :text="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'">
en la esquina inferior derecha del panel para que siga visible
mientras se lee. Ver más abajo en el scroll container. -->
<UTooltip :text="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'">
<UButton <UButton
:icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'" :icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
:color="isFav ? 'primary' : 'neutral'" :color="isFav ? 'primary' : 'neutral'"
@ -393,96 +396,110 @@ function onInputKey(e: KeyboardEvent) {
@click="onToggleFavorite" @click="onToggleFavorite"
/> />
</UTooltip> </UTooltip>
</template> </template>
</UDashboardNavbar> </UDashboardNavbar>
<div class="flex flex-col sm:flex-row justify-between gap-2 p-4 sm:px-6 border-b border-default shadow-lg z-10"> <!-- Metadata + controles -->
<div class="min-w-0 flex gap-1 sm:gap-4 sm:items-center flex-col sm:flex-row"> <div class="flex flex-col gap-3 p-4 sm:px-6 border-b border-default shadow-sm z-10">
<p class=" text-sm text-highlighted flex items-center gap-1 mb-0.5"> <!-- Fila 1: fecha, lugar y enlace externo -->
<UIcon name="ph:calendar" class="size-5 text-green-600" /> <div class="flex flex-wrap items-start justify-between gap-2">
{{ safeDate(activity) }} <div class="flex flex-wrap items-center gap-x-4 gap-y-1 min-w-0">
</p> <p v-if="safeDate(activity)" class="text-sm text-highlighted flex items-center gap-1.5 shrink-0">
<p class="text-sm truncate flex items-center gap-1"> <UIcon name="ph:calendar" class="size-4 text-green-600" />
<UIcon name="ph:map-pin" class="size-5 text-green-600" /> {{ safeDate(activity) }}
{{ formatLocation(activity) }} </p>
</p> <p v-if="formatLocation(activity)" class="text-sm flex items-center gap-1.5 text-muted min-w-0">
<UIcon name="ph:map-pin" class="size-4 text-green-600 shrink-0" />
<span class="truncate max-w-[220px]">{{ formatLocation(activity) }}</span>
</p>
</div>
<UButton
v-if="matchUrl"
:to="matchUrl"
target="_blank"
icon="i-lucide-external-link"
label="Ver en sitio"
color="primary"
variant="soft"
size="xs"
class="shrink-0"
/>
</div> </div>
<!-- Fila 2: archivos adjuntos -->
<div v-if="fileLinks.length" class="flex flex-wrap gap-2">
<UButton
v-for="(f, idx) in fileLinks"
:key="idx"
:to="f.to"
:target="f.target"
:icon="f.icon!"
:label="f.label"
color="neutral"
variant="subtle"
size="xs"
external
/>
</div>
<!-- Fila 3: búsqueda local en el documento -->
<div <div
v-if="activity?.body" v-if="activity?.body"
class="flex items-center gap-2" class="flex items-center gap-2"
> >
<UFieldGroup> <UInput
<UInput v-model="localQuery"
v-model="localQuery" icon="i-lucide-search"
icon="i-lucide-search" placeholder="Buscar en este documento..."
placeholder="Buscar frase exacta en este documento..." class="flex-1 min-w-0"
class="w-full" :ui="{ trailing: 'pe-1' }"
:ui="{ trailing: 'pe-1' }" @keydown="onInputKey"
@keydown="onInputKey"
>
<template #trailing>
<UButton
v-if="localQuery"
icon="i-lucide-x"
variant="link"
aria-label="Limpiar"
@click="clearLocalQuery"
/>
</template>
</UInput>
<UBadge v-if="localQuery" class="bg-gray-400 text-white">
<span
class="text-xs tabular-nums whitespace-nowrap min-w-[3.5rem] text-center text-white"
:class="totalMatches ? 'text-toned font-medium' : 'text-dimmed'"
>
<template v-if="localQuery">
{{ totalMatches ? `${currentIdx + 1} / ${totalMatches}` : '0 / 0' }}
</template>
</span>
</UBadge>
<UTooltip text="Anterior (Shift+Enter)">
<UButton
icon="i-lucide-chevron-up"
:disabled="!totalMatches"
aria-label="Coincidencia anterior"
class="bg-gray-400"
color="neutral"
@click="prevMatch"
/>
</UTooltip>
<UTooltip text="Siguiente (Enter)">
<UButton
icon="i-lucide-chevron-down"
:disabled="!totalMatches"
aria-label="Coincidencia siguiente"
class="bg-gray-400"
color="neutral"
@click="nextMatch"
/>
</UTooltip>
</UFieldGroup>
</div>
<!-- <ul v-if="fileLinks.length" class="flex flex-wrap gap-3 text-xs text-muted">
<li
v-for="(f, idx) in fileLinks"
:key="idx"
class="flex items-center"
> >
<ULink <template #trailing>
:to="f.to" <UButton
:target="f.target" v-if="localQuery"
class="flex items-center gap-1 hover:text-primary" icon="i-lucide-x"
variant="link"
aria-label="Limpiar"
@click="clearLocalQuery"
/>
</template>
</UInput>
<template v-if="localQuery">
<span
class="text-xs tabular-nums whitespace-nowrap shrink-0 px-2 py-1 rounded-md bg-elevated border border-default"
:class="totalMatches ? 'text-toned font-medium' : 'text-dimmed'"
> >
<UIcon :name="f.icon!" class="size-4" /> {{ totalMatches ? `${currentIdx + 1} / ${totalMatches}` : '0 / 0' }}
<span>{{ f.label }}</span> </span>
</ULink>
</li> <div class="flex items-center gap-1 shrink-0">
</ul> --> <UTooltip text="Anterior (Shift+Enter)">
<UButton
icon="i-lucide-chevron-up"
:disabled="!totalMatches"
aria-label="Coincidencia anterior"
color="neutral"
variant="ghost"
size="sm"
@click="prevMatch"
/>
</UTooltip>
<UTooltip text="Siguiente (Enter)">
<UButton
icon="i-lucide-chevron-down"
:disabled="!totalMatches"
aria-label="Coincidencia siguiente"
color="neutral"
variant="ghost"
size="sm"
@click="nextMatch"
/>
</UTooltip>
</div>
</template>
</div>
</div> </div>
<div ref="scrollContainer" class="flex-1 overflow-y-auto relative bg-gray-100"> <div ref="scrollContainer" class="flex-1 overflow-y-auto relative bg-gray-100">

View File

@ -0,0 +1,56 @@
import { watch, onMounted, onBeforeUnmount, toValue } from 'vue'
import type { Ref, WritableComputedRef } from 'vue'
/**
* Intercepts the mobile hardware back button when a detail panel is open,
* closing the panel instead of navigating away from the page.
*
* Strategy: when the panel opens on mobile, push a history entry with the
* same URL. When the user presses back, popstate fires and we close the
* panel. When the user closes via the X button, we programmatically pop
* the entry we pushed so the stack stays clean.
*/
export function useDetailHistory(
isOpen: WritableComputedRef<boolean> | Ref<boolean>,
isMobile: Ref<boolean>
) {
let pushed = false
let fromPopState = false
let suppressNext = false
function handlePopState() {
if (suppressNext) {
suppressNext = false
return
}
if (pushed && toValue(isMobile)) {
fromPopState = true
pushed = false
isOpen.value = false
}
}
watch(isOpen, (open, wasOpen) => {
if (!toValue(isMobile)) return
if (open && !wasOpen) {
history.pushState({ detailPanel: true }, '')
pushed = true
} else if (!open && wasOpen) {
if (fromPopState) {
// Back button closed the panel — state already popped by the browser.
fromPopState = false
} else if (pushed) {
// X button (or any programmatic close) — pop the entry we pushed.
pushed = false
suppressNext = true
history.go(-1)
}
}
})
if (import.meta.client) {
onMounted(() => window.addEventListener('popstate', handlePopState))
onBeforeUnmount(() => window.removeEventListener('popstate', handlePopState))
}
}

View File

@ -109,6 +109,8 @@ watch(hits, () => {
const breakpoints = useBreakpoints(breakpointsTailwind) const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = breakpoints.smaller('lg') const isMobile = breakpoints.smaller('lg')
useDetailHistory(isActivityPanelOpen, isMobile)
function retry() { function retry() {
runSearch(query.value, false) runSearch(query.value, false)
} }

View File

@ -101,6 +101,8 @@ watch(hits, () => {
const breakpoints = useBreakpoints(breakpointsTailwind) const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = breakpoints.smaller('lg') const isMobile = breakpoints.smaller('lg')
useDetailHistory(isPanelOpen, isMobile)
function retry() { function retry() {
runSearch(query.value, false) runSearch(query.value, false)
} }

View File

@ -63,13 +63,23 @@ const loading = ref(false)
const loadingMore = ref(false) const loadingMore = ref(false)
const errorMsg = ref<string | null>(null) const errorMsg = ref<string | null>(null)
interface Study {
id?: number
title?: string
date?: string
place?: string
link?: string
}
interface EntrelineaDoc { interface EntrelineaDoc {
id?: string id?: string
image?: string image?: string
link?: string link?: string
locale?: string locale?: string
origin?: string
page?: number | string page?: number | string
text?: string text?: string
studies?: Study[]
[key: string]: unknown [key: string]: unknown
} }
@ -164,6 +174,7 @@ async function runSearch(q: string, append = false) {
const res = (multi?.results?.[0] ?? {}) as TypesenseSearchResponse const res = (multi?.results?.[0] ?? {}) as TypesenseSearchResponse
const newHits = res?.hits ?? [] const newHits = res?.hits ?? []
hits.value = append ? hits.value.concat(newHits) : newHits hits.value = append ? hits.value.concat(newHits) : newHits
total.value = res?.found ?? hits.value.length total.value = res?.found ?? hits.value.length
currentPage.value = page currentPage.value = page
@ -227,6 +238,8 @@ watch(hits, () => {
const breakpoints = useBreakpoints(breakpointsTailwind) const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = breakpoints.smaller('lg') const isMobile = breakpoints.smaller('lg')
useDetailHistory(isPanelOpen, isMobile)
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* Favoritos */ /* Favoritos */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
@ -320,7 +333,7 @@ function highlightedFor(hit: TypesenseHit, field: string): string | null {
· ·
<code class="text-toned">{{ QUERY_BY }}</code> <code class="text-toned">{{ QUERY_BY }}</code>
· ·
<code class="text-toned">{{ FILTER_BY }}</code> <code class="text-toned">{{ filterBy }}</code>
</span> </span>
</p> </p>
</div> </div>