diff --git a/app/components/inbox/InboxList.vue b/app/components/inbox/InboxList.vue index 3d2671e..395f6b1 100755 --- a/app/components/inbox/InboxList.vue +++ b/app/components/inbox/InboxList.vue @@ -16,6 +16,9 @@ const props = defineProps<{ * Se propaga al sistema de favoritos para distinguir entre tipos de * contenido (actividades, conferencias, etc.). */ collection?: string + /** Muestra el mensaje "No hay más resultados" al llegar al final. + * Poner en false en modo de paginación numerada. */ + showEndMessage?: boolean }>() const favorites = useFavoritesStore() @@ -771,7 +774,7 @@ useIntersectionObserver(
No hay más resultados diff --git a/app/layouts/default.vue b/app/layouts/default.vue index fc0acc0..960ecab 100755 --- a/app/layouts/default.vue +++ b/app/layouts/default.vue @@ -59,6 +59,12 @@ const links = computed(() => { to: '/historial', badge: histTotal.value > 0 ? String(histTotal.value) : undefined, onSelect: () => { open.value = false } + }, + { + label: t("nav.settings"), + icon: 'i-lucide-settings', + to: '/configuracion', + onSelect: () => { open.value = false } } ] satisfies NavigationMenuItem[] }) diff --git a/app/pages/actividades.vue b/app/pages/actividades.vue index 8808ae0..2dd3793 100755 --- a/app/pages/actividades.vue +++ b/app/pages/actividades.vue @@ -3,32 +3,37 @@ 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' +import { useSettingsStore } from '~/stores/settings' const { $i18n } = useNuxtApp(); const t = $i18n.t; -const PAGE_SIZE = 15 const REQUEST_TIMEOUT_MS = 15000 +const settings = useSettingsStore() + const query = ref('') const debouncedQuery = useDebounce(query, 150) const loading = ref(false) const loadingMore = ref(false) const errorMsg = ref(null) -// Use the raw Meilisearch client so we can pass an AbortSignal. const meili = useMeiliSearchRef() const hits = ref([]) const total = ref(0) +const activePage = ref(1) -const hasMore = computed(() => hits.value.length < total.value) +const totalPages = computed(() => Math.max(1, Math.ceil(total.value / settings.pageSize))) + +const hasMore = computed(() => + settings.paginationType === 'infinite_scroll' ? hits.value.length < total.value : false +) let searchSeq = 0 let abortController: AbortController | null = null -async function runSearch(q: string, append = false) { - // Cancel any in-flight search; saves bandwidth and prevents pile-ups. +async function runSearch(q: string, page = 1, append = false) { abortController?.abort() const ac = new AbortController() abortController = ac @@ -38,15 +43,19 @@ async function runSearch(q: string, append = false) { else loading.value = true errorMsg.value = null - // Safety net in case the network just hangs. const timeoutId = setTimeout(() => ac.abort(), REQUEST_TIMEOUT_MS) + const isInfinite = settings.paginationType === 'infinite_scroll' + const offset = isInfinite + ? (append ? hits.value.length : 0) + : (page - 1) * settings.pageSize + try { const res = await meili.index(`activities_${$i18n.locale.value.toUpperCase()}`).search(q || '', { attributesToRetrieve: ['*'], showMatchesPosition: true, - limit: PAGE_SIZE, - offset: append ? hits.value.length : 0, + limit: settings.pageSize, + offset, sort: q ? undefined : ['isodate:desc'] }, { signal: ac.signal }) @@ -55,9 +64,9 @@ async function runSearch(q: string, append = false) { const newHits = (res?.hits ?? []) as SearchHit[] hits.value = append ? hits.value.concat(newHits) : newHits total.value = res?.estimatedTotalHits ?? hits.value.length + if (!append) activePage.value = page } catch (err: unknown) { const name = (err as { name?: string })?.name - // Aborts are expected when the user types fast; don't surface them. if (name === 'AbortError') return if (seq !== searchSeq) return console.error('Meilisearch error', err) @@ -76,12 +85,22 @@ async function runSearch(q: string, append = false) { } function loadMore() { + if (settings.paginationType !== 'infinite_scroll') return if (loadingMore.value || loading.value || !hasMore.value) return - runSearch(query.value, true) + runSearch(query.value, activePage.value, true) } -// Initial fetch on the client (avoids blocking SSR). -onMounted(() => runSearch('')) +function goToPage(p: number) { + activePage.value = p + hits.value = [] + runSearch(query.value, p, false) +} + +function retry() { + runSearch(query.value, activePage.value, false) +} + +onMounted(() => runSearch('', 1, false)) onBeforeUnmount(() => { abortController?.abort() @@ -90,7 +109,8 @@ onBeforeUnmount(() => { watch(debouncedQuery, (q) => { hits.value = [] total.value = 0 - runSearch(q, false) + activePage.value = 1 + runSearch(q, 1, false) }) const selectedActivity = ref(null) @@ -110,10 +130,6 @@ const breakpoints = useBreakpoints(breakpointsTailwind) const isMobile = breakpoints.smaller('lg') useDetailHistory(isActivityPanelOpen, isMobile) - -function retry() { - runSearch(query.value, false) -}