fix bug favoritos
This commit is contained in:
parent
506f50181a
commit
d7f5612954
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import type { SearchHit } from '~/types'
|
||||||
|
|
||||||
|
interface ParagraphDoc {
|
||||||
|
id?: string
|
||||||
|
document_id: string
|
||||||
|
text: string
|
||||||
|
raw?: string
|
||||||
|
number: number
|
||||||
|
locale: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TypesenseParagraphHit {
|
||||||
|
document: ParagraphDoc
|
||||||
|
highlights?: unknown[]
|
||||||
|
highlight?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DocumentDoc {
|
||||||
|
id?: string
|
||||||
|
code: string
|
||||||
|
locale: string
|
||||||
|
type: string
|
||||||
|
title: string
|
||||||
|
timestamp: number
|
||||||
|
date: string
|
||||||
|
place?: string
|
||||||
|
city?: string
|
||||||
|
state?: string
|
||||||
|
country?: string
|
||||||
|
thumbnail?: string
|
||||||
|
files?: { youtube?: string; video?: string; audio?: string; booklet?: string; simple?: string }
|
||||||
|
slug?: string
|
||||||
|
body?: string
|
||||||
|
draft?: boolean
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps the favorites/history collection name → Typesense main + paragraphs collections.
|
||||||
|
const COLLECTION_CONFIG: Record<string, { main: string; paragraphs: string }> = {
|
||||||
|
'bible-studies-ts': { main: 'activities', paragraphs: 'activities_paragraphs' },
|
||||||
|
'conferences-ts': { main: 'conferences', paragraphs: 'conferences_paragraphs' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePublicationFetch() {
|
||||||
|
const detailDocument = ref<DocumentDoc | null>(null)
|
||||||
|
const detailDocumentLoading = ref(false)
|
||||||
|
const detailParagraphs = ref<TypesenseParagraphHit[]>([])
|
||||||
|
const detailParagraphsLoading = ref(false)
|
||||||
|
|
||||||
|
const { documentsApi } = useTypesenseApi()
|
||||||
|
|
||||||
|
async function fetchDetail(hit: SearchHit, favoritesCollection: string) {
|
||||||
|
const config = COLLECTION_CONFIG[favoritesCollection]
|
||||||
|
const docId = String(hit._id || hit.id || '')
|
||||||
|
if (!config || !docId) {
|
||||||
|
detailDocument.value = null
|
||||||
|
detailParagraphs.value = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
detailDocumentLoading.value = true
|
||||||
|
detailParagraphsLoading.value = true
|
||||||
|
detailDocument.value = null
|
||||||
|
detailParagraphs.value = []
|
||||||
|
try {
|
||||||
|
const res = await documentsApi.multiSearch({
|
||||||
|
multiSearchParameters: {},
|
||||||
|
multiSearchSearchesParameter: {
|
||||||
|
searches: [{
|
||||||
|
collection: config.main,
|
||||||
|
q: '*',
|
||||||
|
queryBy: 'title',
|
||||||
|
filterBy: `id:=${docId} && $${config.paragraphs}(id: *)`,
|
||||||
|
includeFields: `*, $${config.paragraphs}(*)`
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const docHit = (res?.results?.[0] as { hits?: Array<{ document: Record<string, unknown> }> })?.hits?.[0]
|
||||||
|
if (docHit) {
|
||||||
|
const docRaw = { ...docHit.document }
|
||||||
|
const rawParagraphs = (docRaw[config.paragraphs] as ParagraphDoc[] | undefined) ?? []
|
||||||
|
delete docRaw[config.paragraphs]
|
||||||
|
detailDocument.value = docRaw as unknown as DocumentDoc
|
||||||
|
detailParagraphs.value = [...rawParagraphs]
|
||||||
|
.sort((a, b) => (a.number ?? 0) - (b.number ?? 0))
|
||||||
|
.map(p => ({ document: p }))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[usePublicationFetch] Error fetching publication detail', err)
|
||||||
|
detailDocument.value = null
|
||||||
|
detailParagraphs.value = []
|
||||||
|
} finally {
|
||||||
|
detailDocumentLoading.value = false
|
||||||
|
detailParagraphsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearDetail() {
|
||||||
|
detailDocument.value = null
|
||||||
|
detailParagraphs.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
detailDocument,
|
||||||
|
detailDocumentLoading,
|
||||||
|
detailParagraphs,
|
||||||
|
detailParagraphsLoading,
|
||||||
|
fetchDetail,
|
||||||
|
clearDetail,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,118 +12,19 @@ const favorites = useFavoritesStore()
|
||||||
// Refs reactivos para usar en watchers / template (Pinia setup store).
|
// Refs reactivos para usar en watchers / template (Pinia setup store).
|
||||||
const { items: favItems, total: favTotal, collections: favCollections } = storeToRefs(favorites)
|
const { items: favItems, total: favTotal, collections: favCollections } = storeToRefs(favorites)
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { locale } = useI18n()
|
|
||||||
const { documentsApi } = useTypesenseApi()
|
|
||||||
|
|
||||||
const TYPESENSE_DOCUMENTS = 'documents'
|
|
||||||
const TYPESENSE_PARAGRAPHS = 'paragraphs'
|
|
||||||
|
|
||||||
/** Identificador de la colección de entrelíneas en el store de favoritos.
|
/** Identificador de la colección de entrelíneas en el store de favoritos.
|
||||||
* Debe coincidir con `FAVORITES_COLLECTION` en `pages/entrelineas.vue`. */
|
* Debe coincidir con `FAVORITES_COLLECTION` en `pages/entrelineas.vue`. */
|
||||||
const ENTRELINEAS_COLLECTION = 'entrelineas'
|
const ENTRELINEAS_COLLECTION = 'entrelineas'
|
||||||
|
|
||||||
interface ParagraphDoc {
|
const {
|
||||||
id?: string
|
detailDocument,
|
||||||
document_id: string
|
detailDocumentLoading,
|
||||||
text: string
|
detailParagraphs,
|
||||||
number: number
|
detailParagraphsLoading,
|
||||||
locale: string
|
fetchDetail,
|
||||||
type: string
|
clearDetail,
|
||||||
}
|
} = usePublicationFetch()
|
||||||
|
|
||||||
interface TypesenseParagraphHit {
|
|
||||||
document: ParagraphDoc
|
|
||||||
highlights?: unknown[]
|
|
||||||
highlight?: Record<string, unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DocumentDoc {
|
|
||||||
id?: string
|
|
||||||
code: string
|
|
||||||
locale: string
|
|
||||||
type: string
|
|
||||||
title: string
|
|
||||||
timestamp: number
|
|
||||||
date: string
|
|
||||||
place?: string
|
|
||||||
city?: string
|
|
||||||
state?: string
|
|
||||||
country?: string
|
|
||||||
thumbnail?: string
|
|
||||||
files?: { youtube?: string; video?: string; audio?: string; booklet?: string; simple?: string }
|
|
||||||
slug?: string
|
|
||||||
body?: string
|
|
||||||
draft?: boolean
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
const detailDocument = ref<DocumentDoc | null>(null)
|
|
||||||
const detailDocumentLoading = ref(false)
|
|
||||||
const detailParagraphs = ref<TypesenseParagraphHit[]>([])
|
|
||||||
const detailParagraphsLoading = ref(false)
|
|
||||||
|
|
||||||
async function fetchDetailDocument(hit: SearchHit) {
|
|
||||||
const docId = String(hit._id || hit.id || '')
|
|
||||||
if (!docId) return
|
|
||||||
detailDocumentLoading.value = true
|
|
||||||
detailDocument.value = null
|
|
||||||
try {
|
|
||||||
const res = await documentsApi.multiSearch({
|
|
||||||
multiSearchParameters: {},
|
|
||||||
multiSearchSearchesParameter: {
|
|
||||||
searches: [{ collection: TYPESENSE_DOCUMENTS, q: '*', queryBy: 'title', filterBy: `id:=${docId}`, perPage: 1, page: 1 }]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const hits = (res?.results?.[0] as { hits?: Array<{ document: DocumentDoc }> })?.hits ?? []
|
|
||||||
detailDocument.value = hits[0]?.document ?? null
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching document', err)
|
|
||||||
detailDocument.value = null
|
|
||||||
} finally {
|
|
||||||
detailDocumentLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchDetailParagraphs(hit: SearchHit) {
|
|
||||||
const docId = String(hit._id || hit.id || '')
|
|
||||||
const type = hit.type as string
|
|
||||||
if (!docId || !type) return
|
|
||||||
detailParagraphsLoading.value = true
|
|
||||||
detailParagraphs.value = []
|
|
||||||
const PER_PAGE = 250
|
|
||||||
let page = 1
|
|
||||||
let totalFound = 0
|
|
||||||
const all: Array<{ document: ParagraphDoc }> = []
|
|
||||||
try {
|
|
||||||
do {
|
|
||||||
const res = await documentsApi.multiSearch({
|
|
||||||
multiSearchParameters: {},
|
|
||||||
multiSearchSearchesParameter: {
|
|
||||||
searches: [{
|
|
||||||
collection: TYPESENSE_PARAGRAPHS,
|
|
||||||
q: '*',
|
|
||||||
queryBy: '',
|
|
||||||
filterBy: `document_id:=${docId} && locale:=${locale.value} && type:=${type}`,
|
|
||||||
perPage: PER_PAGE,
|
|
||||||
page,
|
|
||||||
sortBy: 'number:asc'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const result = (res?.results?.[0] as { found?: number; hits?: Array<{ document: ParagraphDoc }> } | undefined)
|
|
||||||
if (!result) break
|
|
||||||
if (page === 1) totalFound = result.found ?? 0
|
|
||||||
all.push(...(result.hits ?? []))
|
|
||||||
page++
|
|
||||||
} while (all.length < totalFound)
|
|
||||||
detailParagraphs.value = all.map(h => ({ document: h.document }))
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching paragraphs', err)
|
|
||||||
detailParagraphs.value = []
|
|
||||||
} finally {
|
|
||||||
detailParagraphsLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nota: la URL de ImageKit y los presets de transformación viven en
|
// Nota: la URL de ImageKit y los presets de transformación viven en
|
||||||
// `~/utils/entrelineaImage.ts` (auto-importado). EntrelineaDetail los usa
|
// `~/utils/entrelineaImage.ts` (auto-importado). EntrelineaDetail los usa
|
||||||
|
|
@ -226,12 +127,10 @@ const isMobile = breakpoints.smaller('lg')
|
||||||
|
|
||||||
watch(selected, (item) => {
|
watch(selected, (item) => {
|
||||||
if (!item || item.collection === ENTRELINEAS_COLLECTION) {
|
if (!item || item.collection === ENTRELINEAS_COLLECTION) {
|
||||||
detailDocument.value = null
|
clearDetail()
|
||||||
detailParagraphs.value = []
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fetchDetailDocument(item.hit)
|
fetchDetail(item.hit, item.collection)
|
||||||
fetchDetailParagraphs(item.hit)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---- Helpers de fila ---------------------------------------------------
|
// ---- Helpers de fila ---------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { breakpointsTailwind } from '@vueuse/core'
|
||||||
import type { DropdownMenuItem } from '@nuxt/ui'
|
import type { DropdownMenuItem } from '@nuxt/ui'
|
||||||
import type { SearchHit } from '~/types'
|
import type { SearchHit } from '~/types'
|
||||||
import { useHistoryStore, type HistoryItem, HISTORY_LIMIT } from '~/stores/history'
|
import { useHistoryStore, type HistoryItem, HISTORY_LIMIT } from '~/stores/history'
|
||||||
import InboxActivity from '~/components/inbox/InboxActivity.vue'
|
import PublicationDetail from '~/components/PublicationDetail.vue'
|
||||||
import EntrelineaDetail from '~/components/entrelineas/EntrelineaDetail.vue'
|
import EntrelineaDetail from '~/components/entrelineas/EntrelineaDetail.vue'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -20,7 +20,7 @@ import EntrelineaDetail from '~/components/entrelineas/EntrelineaDetail.vue'
|
||||||
* - El store es `useHistoryStore` y los items tienen `visitedAt` (no `addedAt`).
|
* - El store es `useHistoryStore` y los items tienen `visitedAt` (no `addedAt`).
|
||||||
* - Cada fila muestra "Visto: <fecha>" para que se entienda que el orden
|
* - Cada fila muestra "Visto: <fecha>" para que se entienda que el orden
|
||||||
* de la lista es por última visita, no por fecha de creación.
|
* de la lista es por última visita, no por fecha de creación.
|
||||||
* - El registro lo hace el detalle al abrirse (ver `InboxActivity.vue` y
|
* - El registro lo hace el detalle al abrirse (`PublicationDetail.vue` y
|
||||||
* `EntrelineaDetail.vue` — ambos llaman a `history.visit(...)`).
|
* `EntrelineaDetail.vue` — ambos llaman a `history.visit(...)`).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
@ -107,6 +107,23 @@ const isPanelOpen = computed({
|
||||||
set(v: boolean) { if (!v) selected.value = null }
|
set(v: boolean) { if (!v) selected.value = null }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
detailDocument,
|
||||||
|
detailDocumentLoading,
|
||||||
|
detailParagraphs,
|
||||||
|
detailParagraphsLoading,
|
||||||
|
fetchDetail,
|
||||||
|
clearDetail,
|
||||||
|
} = usePublicationFetch()
|
||||||
|
|
||||||
|
watch(selected, (item) => {
|
||||||
|
if (!item || item.collection === ENTRELINEAS_COLLECTION) {
|
||||||
|
clearDetail()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fetchDetail(item.hit, item.collection)
|
||||||
|
})
|
||||||
|
|
||||||
// Si el item seleccionado desaparece del historial (eliminado desde otra
|
// Si el item seleccionado desaparece del historial (eliminado desde otra
|
||||||
// pestaña o desde aquí mismo), cerramos el panel de detalle.
|
// pestaña o desde aquí mismo), cerramos el panel de detalle.
|
||||||
watch(histItems, (items) => {
|
watch(histItems, (items) => {
|
||||||
|
|
@ -546,11 +563,14 @@ const nearLimit = computed(() => histTotal.value >= Math.floor(HISTORY_LIMIT * 0
|
||||||
:collection="selectedCollection"
|
:collection="selectedCollection"
|
||||||
@close="selected = null"
|
@close="selected = null"
|
||||||
/>
|
/>
|
||||||
<!-- Resto (actividades, conferencias) → InboxActivity. -->
|
<!-- Resto (actividades, conferencias) → detalle completo con párrafos. -->
|
||||||
<InboxActivity
|
<PublicationDetail
|
||||||
v-else-if="selected && !isMobile"
|
v-else-if="selected && !isMobile"
|
||||||
:activity="selectedHit!"
|
:document="detailDocument"
|
||||||
:collection="selectedCollection"
|
:document-loading="detailDocumentLoading"
|
||||||
|
:paragraphs="detailParagraphs"
|
||||||
|
:paragraphs-loading="detailParagraphsLoading"
|
||||||
|
:collection="selectedCollection!"
|
||||||
@close="selected = null"
|
@close="selected = null"
|
||||||
/>
|
/>
|
||||||
<div v-else-if="!selected" class="hidden lg:flex flex-1 items-center justify-center">
|
<div v-else-if="!selected" class="hidden lg:flex flex-1 items-center justify-center">
|
||||||
|
|
@ -571,10 +591,13 @@ const nearLimit = computed(() => histTotal.value >= Math.floor(HISTORY_LIMIT * 0
|
||||||
:collection="selectedCollection"
|
:collection="selectedCollection"
|
||||||
@close="selected = null"
|
@close="selected = null"
|
||||||
/>
|
/>
|
||||||
<InboxActivity
|
<PublicationDetail
|
||||||
v-else-if="selected"
|
v-else-if="selected"
|
||||||
:activity="selectedHit!"
|
:document="detailDocument"
|
||||||
:collection="selectedCollection"
|
:document-loading="detailDocumentLoading"
|
||||||
|
:paragraphs="detailParagraphs"
|
||||||
|
:paragraphs-loading="detailParagraphsLoading"
|
||||||
|
:collection="selectedCollection!"
|
||||||
@close="selected = null"
|
@close="selected = null"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue