From d7f56129547a842a68fa79ba6dfbc9b54c22d39a Mon Sep 17 00:00:00 2001 From: David Ascanio Date: Mon, 25 May 2026 19:45:28 -0300 Subject: [PATCH] fix bug favoritos --- app/composables/usePublicationFetch.ts | 112 +++++++++++++++++++++++ app/pages/favoritos.vue | 121 ++----------------------- app/pages/historial.vue | 41 +++++++-- 3 files changed, 154 insertions(+), 120 deletions(-) create mode 100644 app/composables/usePublicationFetch.ts diff --git a/app/composables/usePublicationFetch.ts b/app/composables/usePublicationFetch.ts new file mode 100644 index 0000000..1ee3ef8 --- /dev/null +++ b/app/composables/usePublicationFetch.ts @@ -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 +} + +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 = { + 'bible-studies-ts': { main: 'activities', paragraphs: 'activities_paragraphs' }, + 'conferences-ts': { main: 'conferences', paragraphs: 'conferences_paragraphs' }, +} + +export function usePublicationFetch() { + const detailDocument = ref(null) + const detailDocumentLoading = ref(false) + const detailParagraphs = ref([]) + 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 }> })?.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, + } +} diff --git a/app/pages/favoritos.vue b/app/pages/favoritos.vue index 1f695aa..24a2301 100644 --- a/app/pages/favoritos.vue +++ b/app/pages/favoritos.vue @@ -12,118 +12,19 @@ const favorites = useFavoritesStore() // Refs reactivos para usar en watchers / template (Pinia setup store). const { items: favItems, total: favTotal, collections: favCollections } = storeToRefs(favorites) 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. * Debe coincidir con `FAVORITES_COLLECTION` en `pages/entrelineas.vue`. */ const ENTRELINEAS_COLLECTION = 'entrelineas' -interface ParagraphDoc { - id?: string - document_id: string - text: string - number: number - locale: string - type: string -} - -interface TypesenseParagraphHit { - document: ParagraphDoc - highlights?: unknown[] - highlight?: Record -} - -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(null) -const detailDocumentLoading = ref(false) -const detailParagraphs = ref([]) -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 - } -} +const { + detailDocument, + detailDocumentLoading, + detailParagraphs, + detailParagraphsLoading, + fetchDetail, + clearDetail, +} = usePublicationFetch() // Nota: la URL de ImageKit y los presets de transformación viven en // `~/utils/entrelineaImage.ts` (auto-importado). EntrelineaDetail los usa @@ -226,12 +127,10 @@ const isMobile = breakpoints.smaller('lg') watch(selected, (item) => { if (!item || item.collection === ENTRELINEAS_COLLECTION) { - detailDocument.value = null - detailParagraphs.value = [] + clearDetail() return } - fetchDetailDocument(item.hit) - fetchDetailParagraphs(item.hit) + fetchDetail(item.hit, item.collection) }) // ---- Helpers de fila --------------------------------------------------- diff --git a/app/pages/historial.vue b/app/pages/historial.vue index 2f9ba57..8156396 100644 --- a/app/pages/historial.vue +++ b/app/pages/historial.vue @@ -5,7 +5,7 @@ import { breakpointsTailwind } from '@vueuse/core' import type { DropdownMenuItem } from '@nuxt/ui' import type { SearchHit } from '~/types' 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' /** @@ -20,7 +20,7 @@ import EntrelineaDetail from '~/components/entrelineas/EntrelineaDetail.vue' * - El store es `useHistoryStore` y los items tienen `visitedAt` (no `addedAt`). * - Cada fila muestra "Visto: " para que se entienda que el orden * 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(...)`). */ @@ -107,6 +107,23 @@ const isPanelOpen = computed({ 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 // pestaña o desde aquí mismo), cerramos el panel de detalle. watch(histItems, (items) => { @@ -546,11 +563,14 @@ const nearLimit = computed(() => histTotal.value >= Math.floor(HISTORY_LIMIT * 0 :collection="selectedCollection" @close="selected = null" /> - - +