fix conflicts
This commit is contained in:
parent
4513f4d839
commit
d2031f1ae9
|
|
@ -50,6 +50,43 @@ const bibleStudyInput = ref<number | null>(null)
|
|||
const activeBibleStudies = ref<BibleStudyChip[]>([])
|
||||
const isValidating = ref(false)
|
||||
|
||||
const {
|
||||
query, debouncedQuery, loading, loadingMore, errorMsg,
|
||||
exactSearch, sortMode,
|
||||
groupedHits, visibleGroupCount, visibleGroups, hasMoreVisible, hasMore,
|
||||
browseItems, hasMoreBrowse,
|
||||
displayGroups, activePage, displayTotal, totalPages,
|
||||
runSearch, runBrowse, loadMore, goToPage, retry
|
||||
} = useGroupedTypesenseSearch({
|
||||
paragraphsCollection: props.paragraphsCollection,
|
||||
mainCollection: props.mainCollection,
|
||||
groupByField: props.groupByField,
|
||||
queryBy: QUERY_BY,
|
||||
filterBy: () => {
|
||||
let base = `locale:=${locale.value}`
|
||||
if (activeBibleStudies.value.length > 0) {
|
||||
const ids = activeBibleStudies.value.map(bs => bs.id).join(',')
|
||||
base += ` && $${props.mainCollection}(bible_study:=[${ids}])`
|
||||
}
|
||||
return base
|
||||
},
|
||||
isUnlocked: () => unlocked.value,
|
||||
pageSize: () => settings.pageSize,
|
||||
paginationType: () => settings.paginationType,
|
||||
initialQuery: q0,
|
||||
initialPage: p0
|
||||
})
|
||||
|
||||
function refetchResults() {
|
||||
if (!debouncedQuery.value.trim()) {
|
||||
browseItems.value = []
|
||||
runBrowse(1, false)
|
||||
} else {
|
||||
groupedHits.value = []
|
||||
runSearch(query.value, 1, false)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyBibleStudyFilter() {
|
||||
const val = bibleStudyInput.value
|
||||
if (val === null || val <= 0) return
|
||||
|
|
@ -100,268 +137,40 @@ function clearAllBibleStudyFilters() {
|
|||
refetchResults()
|
||||
}
|
||||
|
||||
function refetchResults() {
|
||||
if (!debouncedQuery.value.trim()) {
|
||||
browseItems.value = []
|
||||
runBrowse(1, false)
|
||||
} else {
|
||||
groupedHits.value = []
|
||||
currentPage.value = 1
|
||||
runSearch(query.value, 1, false)
|
||||
// ---- Types ----------------------------------------------------------------
|
||||
|
||||
interface DocumentDoc extends CachedDocMeta {
|
||||
code: string
|
||||
locale: string
|
||||
files?: {
|
||||
youtube?: string
|
||||
video?: string
|
||||
audio?: string
|
||||
booklet?: string
|
||||
simple?: string
|
||||
}
|
||||
body?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// ---- Colors ----------------------------------------------------------------
|
||||
|
||||
const groupedHits = ref<SearchGroup[]>([])
|
||||
const total = ref(0)
|
||||
const currentPage = ref(1)
|
||||
|
||||
const hasMore = computed(() =>
|
||||
settings.paginationType === 'infinite_scroll' ? groupedHits.value.length < total.value : false
|
||||
)
|
||||
|
||||
const visibleGroupCount = ref(10)
|
||||
|
||||
const visibleGroups = computed(() =>
|
||||
settings.paginationType === 'infinite_scroll'
|
||||
? groupedHits.value.slice(0, visibleGroupCount.value)
|
||||
: groupedHits.value
|
||||
)
|
||||
|
||||
const hasMoreVisible = computed(() =>
|
||||
settings.paginationType === 'infinite_scroll' &&
|
||||
visibleGroupCount.value < groupedHits.value.length
|
||||
)
|
||||
|
||||
const browseItems = ref<BrowseItem[]>([])
|
||||
const browseTotal = ref(0)
|
||||
const browsePage = ref(1)
|
||||
|
||||
const hasMoreBrowse = computed(() =>
|
||||
settings.paginationType === 'infinite_scroll'
|
||||
? browseItems.value.length < browseTotal.value
|
||||
: false
|
||||
)
|
||||
|
||||
const displayGroups = computed((): DisplayGroup[] => {
|
||||
if (!debouncedQuery.value.trim()) {
|
||||
return browseItems.value.map(item => ({
|
||||
docId: item.docId,
|
||||
meta: item.meta,
|
||||
firstHit: null
|
||||
}))
|
||||
const colors = computed(() => {
|
||||
if (props.accentColor === 'green') {
|
||||
return {
|
||||
selectedItem: 'border-carpagreen bg-carpagreen/10',
|
||||
hoverItem: 'border-gray-200 hover:border-carpagreen hover:bg-carpagreen/5',
|
||||
icon: 'text-carpagreen',
|
||||
}
|
||||
}
|
||||
return {
|
||||
selectedItem: 'border-carpablue bg-carpablue/10',
|
||||
hoverItem: 'border-gray-200 hover:border-carpablue hover:bg-carpablue/5',
|
||||
icon: 'text-carpablue',
|
||||
}
|
||||
return visibleGroups.value.map(g => ({
|
||||
docId: g.docId,
|
||||
meta: docCache.value[g.docId],
|
||||
firstHit: g.firstHit
|
||||
}))
|
||||
})
|
||||
|
||||
const activePage = ref(p0)
|
||||
|
||||
const displayTotal = computed(() =>
|
||||
debouncedQuery.value.trim() ? total.value : browseTotal.value
|
||||
)
|
||||
|
||||
const totalPages = computed(() =>
|
||||
Math.max(1, Math.ceil(displayTotal.value / settings.pageSize))
|
||||
)
|
||||
|
||||
const docCache = ref<Record<string, DocMeta>>({})
|
||||
|
||||
const { documentsApi } = useTypesenseApi()
|
||||
const toast = useToast()
|
||||
|
||||
// ---- Batch fetch de metadatos ---------------------------------------------
|
||||
|
||||
async function fetchDocumentMeta(docIds: string[]) {
|
||||
const unique = docIds.filter(id => id && !(id in docCache.value))
|
||||
if (!unique.length) return
|
||||
try {
|
||||
const res = await documentsApi.multiSearch({
|
||||
multiSearchParameters: {},
|
||||
multiSearchSearchesParameter: {
|
||||
searches: [{
|
||||
collection: props.mainCollection,
|
||||
q: '*',
|
||||
queryBy: 'title',
|
||||
filterBy: `id:=[${unique.join(',')}]${unlocked.value ? '' : ' && private:=false'}`,
|
||||
includeFields: 'id,title,date,timestamp,place,city,state,country,type,slug,draft',
|
||||
perPage: unique.length,
|
||||
page: 1
|
||||
}]
|
||||
}
|
||||
})
|
||||
const docHits = (res?.results?.[0] as { hits?: Array<{ document: DocMeta }> })?.hits ?? []
|
||||
for (const hit of docHits) {
|
||||
if (hit.document.id) docCache.value[hit.document.id] = hit.document
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching document metadata', err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Búsqueda de párrafos (con query) -------------------------------------
|
||||
|
||||
let searchSeq = 0
|
||||
let timeoutId: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
async function runSearch(q: string, page = 1, append = false) {
|
||||
const seq = ++searchSeq
|
||||
if (append) loadingMore.value = true
|
||||
else loading.value = true
|
||||
errorMsg.value = null
|
||||
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => {
|
||||
if (seq === searchSeq) {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
errorMsg.value = 'La búsqueda tardó demasiado. Inténtalo de nuevo.'
|
||||
}
|
||||
}, REQUEST_TIMEOUT_MS)
|
||||
|
||||
const isInfinite = settings.paginationType === 'infinite_scroll'
|
||||
const typePage = isInfinite ? (append ? currentPage.value + 1 : 1) : page
|
||||
|
||||
try {
|
||||
const shouldSortByDate = sortMode.value === 'date' && q.trim()
|
||||
|
||||
const multi = await documentsApi.multiSearch({
|
||||
multiSearchParameters: {},
|
||||
multiSearchSearchesParameter: {
|
||||
searches: [{
|
||||
collection: props.paragraphsCollection,
|
||||
q: exactSearch.value && q ? `"${q}"` : q || '*',
|
||||
queryBy: QUERY_BY,
|
||||
filterBy: unlocked.value ? filterBy.value : `${filterBy.value} && $${props.mainCollection}(private:=false)`,
|
||||
...(shouldSortByDate ? { sortBy: `$${props.mainCollection}(timestamp:desc)` } : {}),
|
||||
perPage: settings.pageSize,
|
||||
page: typePage,
|
||||
highlightFullFields: QUERY_BY,
|
||||
highlightFields: QUERY_BY,
|
||||
highlightStartTag: '<mark class="search-match">',
|
||||
highlightEndTag: '</mark>',
|
||||
highlightAffixNumTokens: 30,
|
||||
groupBy: props.groupByField
|
||||
}]
|
||||
}
|
||||
})
|
||||
if (seq !== searchSeq) return
|
||||
|
||||
const res = (multi?.results?.[0] ?? {}) as TypesenseSearchResponse
|
||||
const rawGroups = res?.groupedHits ?? []
|
||||
const newGroups: SearchGroup[] = rawGroups.map(g => ({
|
||||
docId: g.groupKey[0]!,
|
||||
firstHit: g.hits[0]!,
|
||||
allHits: g.hits
|
||||
}))
|
||||
|
||||
if (!append) docCache.value = {}
|
||||
await fetchDocumentMeta(newGroups.map(g => g.docId).filter(Boolean))
|
||||
|
||||
if (seq !== searchSeq) return
|
||||
|
||||
groupedHits.value = append ? groupedHits.value.concat(newGroups) : newGroups
|
||||
total.value = res?.found ?? groupedHits.value.length
|
||||
currentPage.value = typePage
|
||||
if (!append) activePage.value = page
|
||||
} catch (err: unknown) {
|
||||
if (seq !== searchSeq) return
|
||||
console.error('Typesense error', err)
|
||||
errorMsg.value = (err as Error)?.message || 'Error al buscar.'
|
||||
if (!append) { groupedHits.value = []; total.value = 0 }
|
||||
} finally {
|
||||
if (seq === searchSeq) {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Exploración por fecha (sin query) ------------------------------------
|
||||
|
||||
async function runBrowse(page = 1, append = false) {
|
||||
const seq = ++searchSeq
|
||||
if (append) loadingMore.value = true
|
||||
else loading.value = true
|
||||
errorMsg.value = null
|
||||
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
timeoutId = setTimeout(() => {
|
||||
if (seq === searchSeq) {
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
errorMsg.value = 'La búsqueda tardó demasiado. Inténtalo de nuevo.'
|
||||
}
|
||||
}, REQUEST_TIMEOUT_MS)
|
||||
|
||||
const isInfinite = settings.paginationType === 'infinite_scroll'
|
||||
const typePage = isInfinite ? (append ? browsePage.value + 1 : 1) : page
|
||||
|
||||
try {
|
||||
const multi = await documentsApi.multiSearch({
|
||||
multiSearchParameters: {},
|
||||
multiSearchSearchesParameter: {
|
||||
searches: [{
|
||||
collection: props.paragraphsCollection,
|
||||
q: '*',
|
||||
queryBy: QUERY_BY,
|
||||
filterBy: `${filterBy.value} && $${props.mainCollection}(locale:=${locale.value}${unlocked.value ? '' : ' && private:=false'})`,
|
||||
sortBy: `$${props.mainCollection}(timestamp:desc)`,
|
||||
groupBy: props.groupByField,
|
||||
perPage: settings.pageSize,
|
||||
page: typePage,
|
||||
includeFields: `$${props.mainCollection}(id,title,date,timestamp,place,city,state,country,type,slug,draft)`
|
||||
}]
|
||||
}
|
||||
})
|
||||
if (seq !== searchSeq) return
|
||||
const result = (multi?.results?.[0] as TypesenseSearchResponse | undefined)
|
||||
const rawGroups = result?.groupedHits ?? []
|
||||
const newItems = rawGroups.map(g => {
|
||||
const docId = g.groupKey[0]!
|
||||
const parentMeta = (g.hits[0]?.document as unknown as Record<string, unknown>)[props.mainCollection] as Partial<DocMeta> | undefined
|
||||
return { docId, meta: { id: docId, ...parentMeta } as DocMeta }
|
||||
})
|
||||
|
||||
browseItems.value = append ? browseItems.value.concat(newItems) : newItems
|
||||
browseTotal.value = result?.found ?? browseItems.value.length
|
||||
browsePage.value = typePage
|
||||
if (!append) activePage.value = page
|
||||
} catch (err: unknown) {
|
||||
if (seq !== searchSeq) return
|
||||
console.error('Typesense error', err)
|
||||
errorMsg.value = (err as Error)?.message || 'Error al buscar.'
|
||||
if (!append) { browseItems.value = []; browseTotal.value = 0 }
|
||||
} finally {
|
||||
if (seq === searchSeq) {
|
||||
if (timeoutId) clearTimeout(timeoutId)
|
||||
loading.value = false
|
||||
loadingMore.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadMore() {
|
||||
if (settings.paginationType !== 'infinite_scroll') return
|
||||
if (loadingMore.value || loading.value || !hasMore.value) return
|
||||
runSearch(query.value, currentPage.value, true)
|
||||
}
|
||||
|
||||
function goToPage(p: number) {
|
||||
activePage.value = p
|
||||
if (!debouncedQuery.value.trim()) {
|
||||
browseItems.value = []
|
||||
runBrowse(p, false)
|
||||
} else {
|
||||
groupedHits.value = []
|
||||
runSearch(query.value, p, false)
|
||||
}
|
||||
}
|
||||
// ---- Scroll infinito y detalle ---------------------------------------------
|
||||
|
||||
const listContainer = ref<HTMLElement | null>(null)
|
||||
|
||||
|
|
@ -379,10 +188,6 @@ function onListScroll() {
|
|||
}
|
||||
}
|
||||
|
||||
function isAbortError(err: unknown): boolean {
|
||||
return (err as { name?: string } | null)?.name === 'AbortError'
|
||||
}
|
||||
|
||||
// ---- Selección y carga del detalle ----------------------------------------
|
||||
|
||||
const selectedDocId = ref<string | null>(null)
|
||||
|
|
@ -421,7 +226,7 @@ async function fetchDocumentWithParagraphs(docId: string) {
|
|||
}
|
||||
} catch (err) {
|
||||
if (seq !== detailSeq) return
|
||||
if (isAbortError(err)) return
|
||||
if ((err as { name?: string })?.name === 'AbortError') return
|
||||
console.error('Error fetching document with paragraphs', err)
|
||||
selectedDocument.value = null
|
||||
selectedParagraphs.value = []
|
||||
|
|
@ -575,7 +380,6 @@ function metaLocation(meta: CachedDocMeta | undefined): string {
|
|||
</div>
|
||||
|
||||
<!-- ─── CHIPS ACTIVOS (solo desktop: fuera del FiltersContainer) ─── -->
|
||||
<!-- En desktop los chips van debajo del input; en mobile van DENTRO del slideover -->
|
||||
<div v-if="showBibleStudyFilter && activeBibleStudies.length > 0 && !isMobile" class="px-4 sm:px-6 py-2 border-b border-default">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<UBadge
|
||||
|
|
@ -623,19 +427,11 @@ function metaLocation(meta: CachedDocMeta | undefined): string {
|
|||
</div>
|
||||
|
||||
<!-- ─── FILTROS: AGREGAR NUEVOS (acordeón desktop / slideover mobile) ─── -->
|
||||
<!--
|
||||
El componente FiltersContainer renderiza DISTINTO según la pantalla:
|
||||
- Desktop (>lg): acordeón colapsable con header "Filtros"
|
||||
- Mobile (<lg): botón "Filtros" que abre un USlideover
|
||||
El slot #content (input + botón) es el MISMO en ambos casos.
|
||||
El slot #chips se usa solo en mobile (dentro del slideover debajo del content).
|
||||
-->
|
||||
<template v-if="showBibleStudyFilter">
|
||||
<FiltersContainer
|
||||
:active-count="activeBibleStudies.length"
|
||||
title="search.filters"
|
||||
>
|
||||
<!-- #chips se renderiza dentro del slideover en mobile -->
|
||||
<template #chips>
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<UBadge
|
||||
|
|
@ -677,7 +473,7 @@ function metaLocation(meta: CachedDocMeta | undefined): string {
|
|||
:placeholder="$t('search.bible_study_placeholder')"
|
||||
size="sm"
|
||||
class="w-36"
|
||||
@keyup.enter="applyBibleStudyFilter(props.mainCollection)"
|
||||
@keyup.enter="applyBibleStudyFilter"
|
||||
/>
|
||||
<UButton
|
||||
size="sm"
|
||||
|
|
@ -685,7 +481,7 @@ function metaLocation(meta: CachedDocMeta | undefined): string {
|
|||
variant="outline"
|
||||
:loading="isValidating"
|
||||
:disabled="bibleStudyInput === null || bibleStudyInput <= 0"
|
||||
@click="applyBibleStudyFilter(props.mainCollection)"
|
||||
@click="applyBibleStudyFilter"
|
||||
>
|
||||
+ {{ $t('search.filter') }}
|
||||
</UButton>
|
||||
|
|
|
|||
Loading…
Reference in New Issue