Compare commits
35 Commits
572cf9031c
...
161ab1dab4
| Author | SHA1 | Date |
|---|---|---|
|
|
161ab1dab4 | |
|
|
7f92da2607 | |
|
|
73cd1ead70 | |
|
|
50215fc657 | |
|
|
87f4f04d2c | |
|
|
0ea3bd0859 | |
|
|
47c88c22a6 | |
|
|
6b7f9bdd78 | |
|
|
0a991082f5 | |
|
|
c827b3e711 | |
|
|
95f29d2fe2 | |
|
|
9082b46717 | |
|
|
2dd21ffe45 | |
|
|
ce8efe5a8a | |
|
|
f817d20e5b | |
|
|
46205886ca | |
|
|
d71334089f | |
|
|
7d3a97d8f0 | |
|
|
1355c93974 | |
|
|
1c5a3e510f | |
|
|
0ed5906f1b | |
|
|
898aa04071 | |
|
|
409ef2cae7 | |
|
|
9f7d868d2b | |
|
|
b7b12184db | |
|
|
364fbe0ad7 | |
|
|
3d54899b3e | |
|
|
1756aadaff | |
|
|
e2deaa8066 | |
|
|
d7f5612954 | |
|
|
506f50181a | |
|
|
f5ba0531c5 | |
|
|
1ce67d7e42 | |
|
|
a09280f915 | |
|
|
e882a41510 |
|
|
@ -22,3 +22,15 @@ logs
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
.bruno
|
||||||
|
|
||||||
|
# Claude local settings
|
||||||
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# Bruno sensitive environments and collections
|
||||||
|
bruno/environments/Produccion.bru
|
||||||
|
bruno/Collections/entrelineas.bru
|
||||||
|
bruno/Collections/entrelineas_add_draft_field.bru
|
||||||
|
bruno/Collections/entrelineas_create_collection.bru
|
||||||
|
bruno/Collections/entrelineas_delete_collection.bru
|
||||||
|
bruno/Collections/entrelineas_documents.bru
|
||||||
|
|
|
||||||
137
app/app.vue
137
app/app.vue
|
|
@ -1,4 +1,11 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const nuxtApp = useNuxtApp();
|
||||||
|
const { $i18n } = useNuxtApp();
|
||||||
|
const t = $i18n.t;
|
||||||
|
|
||||||
|
const title = t('seo.title')
|
||||||
|
const description = t('seo.description')
|
||||||
|
|
||||||
useHead({
|
useHead({
|
||||||
meta: [
|
meta: [
|
||||||
{ charset: 'utf-8' },
|
{ charset: 'utf-8' },
|
||||||
|
|
@ -8,19 +15,141 @@ useHead({
|
||||||
{ rel: 'icon', href: '/favicon.ico' }
|
{ rel: 'icon', href: '/favicon.ico' }
|
||||||
],
|
],
|
||||||
htmlAttrs: {
|
htmlAttrs: {
|
||||||
lang: 'es'
|
lang: $i18n.locale
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const title = 'La Gran Carpa Catedral - Buscador'
|
|
||||||
const description = 'Buscador de actividades y conferencias.'
|
|
||||||
|
|
||||||
useSeoMeta({
|
useSeoMeta({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
ogTitle: title,
|
ogTitle: title,
|
||||||
ogDescription: description
|
ogDescription: description
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
element: "#bible-studies",
|
||||||
|
popover: {
|
||||||
|
title: t('nav.bible_studies'),
|
||||||
|
description: t('tour.bible_studies_description'),
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "#conferences",
|
||||||
|
popover: {
|
||||||
|
title: t('nav.conferences'),
|
||||||
|
description: t('tour.conferences_description'),
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "#betweenthelines",
|
||||||
|
popover: {
|
||||||
|
title: t('nav.between_the_lines'),
|
||||||
|
description: t('tour.betweenthelines_description'),
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "#favorites",
|
||||||
|
popover: {
|
||||||
|
title: t('nav.my_list'),
|
||||||
|
description: t('tour.favorites_description'),
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "#history",
|
||||||
|
popover: {
|
||||||
|
title: t('nav.history'),
|
||||||
|
description: t('tour.history_description'),
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "#changelog",
|
||||||
|
popover: {
|
||||||
|
title: t('nav.changelog'),
|
||||||
|
description: t('tour.changelog_description'),
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "#settings",
|
||||||
|
popover: {
|
||||||
|
title: t('nav.settings'),
|
||||||
|
description: t('tour.settings_description'),
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "#feedback",
|
||||||
|
popover: {
|
||||||
|
title: t('feedback.title'),
|
||||||
|
description: t('tour.feedback_description'),
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "#localeSelector",
|
||||||
|
popover: {
|
||||||
|
title: t('nav.localeselector'),
|
||||||
|
description: t('tour.localeselector_description'),
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: ".collapse-sidebar-icon",
|
||||||
|
popover: {
|
||||||
|
title: t('search.collapse'),
|
||||||
|
description: t('tour.collapse_sidebar_description'),
|
||||||
|
side: "bottom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: ".total-results",
|
||||||
|
popover: {
|
||||||
|
title: t('search.total_results'),
|
||||||
|
description: t('tour.total_results_description'),
|
||||||
|
side: "bottom"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: "#index_changelog",
|
||||||
|
popover: {
|
||||||
|
title: t('nav.changelog'),
|
||||||
|
description: t('tour.changelog_description'),
|
||||||
|
side: "right",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: ".favorites_toggle",
|
||||||
|
popover: {
|
||||||
|
title: t('tour.favorites_button'),
|
||||||
|
description: t('tour.favorites_toggle'),
|
||||||
|
side: "bottom"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const tourConfig = {
|
||||||
|
nextBtnText: t('tour.next'),
|
||||||
|
prevBtnText: t('tour.prev'),
|
||||||
|
doneBtnText: t('tour.done'),
|
||||||
|
progressText: t('tour.progress', { current: '{{current}}', total: '{{total}}' }),
|
||||||
|
showProgress: true,
|
||||||
|
animate: true,
|
||||||
|
smoothScroll: true,
|
||||||
|
steps: steps,
|
||||||
|
}
|
||||||
|
|
||||||
|
nuxtApp.hook('tour', (startIndex=0)=>{
|
||||||
|
const { driver } = useDriver("onboarding");
|
||||||
|
const instance = driver( tourConfig )
|
||||||
|
instance.drive(startIndex)
|
||||||
|
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@
|
||||||
--color-carpablue: #2C4EA2;
|
--color-carpablue: #2C4EA2;
|
||||||
--color-carpagreen: #6B8E23;
|
--color-carpagreen: #6B8E23;
|
||||||
--color-carpared: #ff0000;
|
--color-carpared: #ff0000;
|
||||||
|
|
||||||
|
--color-primary: #6B8E23;
|
||||||
|
--color-secondary: #2C4EA2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Colors for Typesense rich text */
|
/* Colors for Typesense rich text */
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
const t = $i18n.t
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
modelValue: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
function sanitize(text: string): string {
|
||||||
|
let clean = text
|
||||||
|
clean = clean.replace(/<[^>]*>/g, '')
|
||||||
|
clean = clean.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||||
|
clean = clean.replace(/https?:\/\/[^\s]+/gi, '')
|
||||||
|
clean = clean.replace(/www\.[^\s]+/gi, '')
|
||||||
|
clean = clean.replace(/javascript\s*:/gi, '')
|
||||||
|
clean = clean.replace(/\beval\s*\(/gi, '')
|
||||||
|
clean = clean.replace(/\bon\w+\s*=/gi, '')
|
||||||
|
clean = clean.replace(/[A-Za-z0-9+/]{50,}={0,2}/g, '')
|
||||||
|
clean = clean.replace(/'\s*OR\s+\d+\s*=\s*\d+/gi, '')
|
||||||
|
clean = clean.replace(/'\s*--/g, "'")
|
||||||
|
clean = clean.replace(/'\s*;\s*DROP\s+TABLE/gi, "'")
|
||||||
|
clean = clean.replace(/UNION\s+SELECT/gi, '')
|
||||||
|
clean = clean.replace(/xp_cmdshell/gi, '')
|
||||||
|
clean = clean.replace(/EXEC\s*\(/gi, '')
|
||||||
|
clean = clean.replace(/`[^`]*`/g, '')
|
||||||
|
clean = clean.replace(/\$\(.*?\)/g, '')
|
||||||
|
clean = clean.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '')
|
||||||
|
return clean.slice(0, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInput(value: string) {
|
||||||
|
emit('update:modelValue', sanitize(value))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UCard class="w-full max-w-xl shadow-sm">
|
||||||
|
<div class="flex flex-col items-center gap-4 py-4">
|
||||||
|
<UIcon name="i-lucide-bug" class="size-10 text-carpablue" />
|
||||||
|
|
||||||
|
<div class="text-center space-y-1">
|
||||||
|
<h2 class="text-lg font-semibold text-highlighted">
|
||||||
|
{{ t('feedback.title') }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-muted leading-relaxed max-w-sm">
|
||||||
|
{{ t('feedback.description') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UTextarea
|
||||||
|
:model-value="modelValue"
|
||||||
|
:placeholder="t('feedback.placeholder')"
|
||||||
|
:rows="6"
|
||||||
|
:maxlength="500"
|
||||||
|
autoresize
|
||||||
|
size="lg"
|
||||||
|
class="w-full"
|
||||||
|
@update:model-value="onInput($event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between w-full text-xs text-muted">
|
||||||
|
<span>{{ t('feedback.anonymous_notice') }}</span>
|
||||||
|
<span>{{ modelValue.length }}/500</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
@ -6,6 +6,7 @@ import { useHistoryStore } from '~/stores/history'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useSettingsStore } from '~/stores/settings'
|
import { useSettingsStore } from '~/stores/settings'
|
||||||
import type { SearchHit } from '~/types'
|
import type { SearchHit } from '~/types'
|
||||||
|
import { pageHeader, select } from '#build/ui'
|
||||||
|
|
||||||
interface TypesenseHighlight {
|
interface TypesenseHighlight {
|
||||||
field?: string
|
field?: string
|
||||||
|
|
@ -17,6 +18,7 @@ interface ParagraphDoc {
|
||||||
id?: string
|
id?: string
|
||||||
document_id: string
|
document_id: string
|
||||||
text: string
|
text: string
|
||||||
|
raw?: string
|
||||||
number: number
|
number: number
|
||||||
locale: string
|
locale: string
|
||||||
type: string
|
type: string
|
||||||
|
|
@ -63,12 +65,17 @@ const props = defineProps<{
|
||||||
query?: string
|
query?: string
|
||||||
selectedHit?: TypesenseParagraphHit | null
|
selectedHit?: TypesenseParagraphHit | null
|
||||||
selectedMatchingHits?: TypesenseParagraphHit[] | null
|
selectedMatchingHits?: TypesenseParagraphHit[] | null
|
||||||
|
accentColor?: 'green' | 'blue'
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emits = defineEmits(['close'])
|
const emits = defineEmits(['close'])
|
||||||
|
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
const favorites = useFavoritesStore()
|
const favorites = useFavoritesStore()
|
||||||
|
|
||||||
|
const iconColor = computed(() => props.accentColor === 'blue' ? 'text-carpablue' : 'text-carpagreen')
|
||||||
const history = useHistoryStore()
|
const history = useHistoryStore()
|
||||||
const { showParagraphNumbers } = storeToRefs(useSettingsStore())
|
const { showParagraphNumbers } = storeToRefs(useSettingsStore())
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
@ -327,6 +334,7 @@ function normalize(s: string): string {
|
||||||
// ---- Refs de estado ---------------------------------------------------------
|
// ---- Refs de estado ---------------------------------------------------------
|
||||||
|
|
||||||
const paragraphsContainer = ref<HTMLElement | null>(null)
|
const paragraphsContainer = ref<HTMLElement | null>(null)
|
||||||
|
const scrollContainer = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
// 'typesense' = Estado 1 (server-driven), 'local' = Estado 2 (client-driven)
|
// 'typesense' = Estado 1 (server-driven), 'local' = Estado 2 (client-driven)
|
||||||
type SearchMode = 'typesense' | 'local'
|
type SearchMode = 'typesense' | 'local'
|
||||||
|
|
@ -339,7 +347,7 @@ const currentMatchIdx = ref(0)
|
||||||
function clearMatchMarks() {
|
function clearMatchMarks() {
|
||||||
const container = paragraphsContainer.value
|
const container = paragraphsContainer.value
|
||||||
if (!container) return
|
if (!container) return
|
||||||
container.querySelectorAll('mark.search-match').forEach(m => {
|
container.querySelectorAll('mark.search-match').forEach((m) => {
|
||||||
const parent = m.parentNode
|
const parent = m.parentNode
|
||||||
if (parent) {
|
if (parent) {
|
||||||
parent.replaceChild(document.createTextNode(m.textContent || ''), m)
|
parent.replaceChild(document.createTextNode(m.textContent || ''), m)
|
||||||
|
|
@ -481,9 +489,11 @@ async function applyTypesenseHighlights() {
|
||||||
matchElements.value = domMarks
|
matchElements.value = domMarks
|
||||||
currentMatchIdx.value = 0
|
currentMatchIdx.value = 0
|
||||||
|
|
||||||
// Paso 4: scroll — solo cuando hay hits de Typesense que ubican el párrafo correcto.
|
// Paso 4: scroll — sin hits (browse mode) volver al inicio; con hits, ir al párrafo correcto.
|
||||||
// Sin hits, los marks se muestran para que el usuario navegue, pero no se hace scroll.
|
if (!hasMatchingHits) {
|
||||||
if (!hasMatchingHits) return
|
if (scrollContainer.value) scrollContainer.value.scrollTop = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let targetMark: HTMLElement | null = snippetMarks[0] ?? null
|
let targetMark: HTMLElement | null = snippetMarks[0] ?? null
|
||||||
|
|
||||||
|
|
@ -582,6 +592,21 @@ function clearLocalQuery() {
|
||||||
|
|
||||||
const localQuery = ref('')
|
const localQuery = ref('')
|
||||||
const debouncedLocalQuery = useDebounce(localQuery, 200)
|
const debouncedLocalQuery = useDebounce(localQuery, 200)
|
||||||
|
const showInternalSearch = ref(true)
|
||||||
|
const internalSearchRef = ref<{ input?: HTMLInputElement } | null>(null)
|
||||||
|
const metaExpanded = ref(true)
|
||||||
|
|
||||||
|
function toggleInternalSearch() {
|
||||||
|
showInternalSearch.value = !showInternalSearch.value
|
||||||
|
if (showInternalSearch.value) {
|
||||||
|
metaExpanded.value = true
|
||||||
|
nextTick(() => internalSearchRef.value?.input?.focus())
|
||||||
|
} else {
|
||||||
|
clearLocalQuery()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.document?.id, () => { metaExpanded.value = true; showInternalSearch.value = true })
|
||||||
|
|
||||||
watch(debouncedLocalQuery, (q) => {
|
watch(debouncedLocalQuery, (q) => {
|
||||||
// Cualquier cambio en el input activa el Estado 2
|
// Cualquier cambio en el input activa el Estado 2
|
||||||
|
|
@ -627,6 +652,12 @@ onMounted(async () => {
|
||||||
if (props.paragraphs.length) {
|
if (props.paragraphs.length) {
|
||||||
await applyTypesenseHighlights()
|
await applyTypesenseHighlights()
|
||||||
}
|
}
|
||||||
|
document.addEventListener('selectionchange', onSelectionChange)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('selectionchange', onSelectionChange)
|
||||||
|
if (selectionTimer) clearTimeout(selectionTimer)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ---- Utilerías de búsqueda local --------------------------------------------
|
// ---- Utilerías de búsqueda local --------------------------------------------
|
||||||
|
|
@ -746,13 +777,55 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
|
||||||
}
|
}
|
||||||
return matches.length
|
return matches.length
|
||||||
}
|
}
|
||||||
|
const isPopOverOpen = ref(false)
|
||||||
|
const selectionTooltip = ref<{ x: number; y: number } | null>(null)
|
||||||
|
let selectionTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
function handleSelection(event: MouseEvent) {
|
||||||
|
const selection = window.getSelection()
|
||||||
|
if (selection && selection.toString().trim().length > 3) {
|
||||||
|
if (selectionTimer) clearTimeout(selectionTimer)
|
||||||
|
selectionTimer = setTimeout(() => {
|
||||||
|
const x = Math.min(Math.max(event.clientX, 96), window.innerWidth - 96)
|
||||||
|
selectionTooltip.value = { x, y: event.clientY }
|
||||||
|
isPopOverOpen.value = true
|
||||||
|
}, 320)
|
||||||
|
} else {
|
||||||
|
clearSelectionTooltip()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelectionTooltip() {
|
||||||
|
if (selectionTimer) { clearTimeout(selectionTimer); selectionTimer = null }
|
||||||
|
selectionTooltip.value = null
|
||||||
|
isPopOverOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectionChange() {
|
||||||
|
const sel = window.getSelection()
|
||||||
|
if (!sel || sel.toString().trim().length === 0) clearSelectionTooltip()
|
||||||
|
}
|
||||||
|
|
||||||
|
const links = computed(() => [])
|
||||||
|
|
||||||
|
const items = computed(() => {
|
||||||
|
return [
|
||||||
|
|
||||||
|
]
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UDashboardPanel id="estudios-ts-detail">
|
<UDashboardPanel id="estudios-ts-detail">
|
||||||
<UDashboardNavbar :toggle="false">
|
<UDashboardNavbar :toggle="false">
|
||||||
<template #leading>
|
<template #leading>
|
||||||
<UButton icon="i-lucide-arrow-left" color="neutral" variant="ghost" class="-ms-1.5" @click="emits('close')" />
|
<UButton
|
||||||
|
icon="i-lucide-arrow-left"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
class="-ms-1.5"
|
||||||
|
@click="emits('close')"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #title>
|
<template #title>
|
||||||
|
|
@ -762,16 +835,49 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #right>
|
<template #right>
|
||||||
|
<UTooltip :text="showInternalSearch ? 'Cerrar buscador' : 'Buscar en documento'">
|
||||||
|
<UButton
|
||||||
|
icon="i-lucide-search"
|
||||||
|
:color="showInternalSearch || !!localQuery ? 'primary' : 'neutral'"
|
||||||
|
:variant="showInternalSearch || !!localQuery ? 'soft' : 'ghost'"
|
||||||
|
:disabled="!paragraphs.length"
|
||||||
|
@click="toggleInternalSearch"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
<UTooltip text="Como funciona?">
|
||||||
|
<UButton
|
||||||
|
icon="ph-student"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
:disabled="!document"
|
||||||
|
@click="nuxtApp.callHook('tour',12)"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
<UTooltip :text="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'">
|
<UTooltip :text="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'">
|
||||||
<UButton :icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
|
<UButton
|
||||||
:color="isFav ? 'primary' : 'neutral'" variant="ghost" :disabled="!document"
|
:icon="isFav ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
|
||||||
:aria-label="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'" @click="onToggleFavorite" />
|
:color="isFav ? 'primary' : 'neutral'"
|
||||||
|
variant="ghost"
|
||||||
|
:disabled="!document"
|
||||||
|
:aria-label="isFav ? 'Quitar de mi lista' : 'Guardar en mi lista'"
|
||||||
|
class="favorites_toggle"
|
||||||
|
@click="onToggleFavorite"
|
||||||
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</template>
|
</template>
|
||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<!-- Strip siempre visible para expandir/colapsar metadatos -->
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center justify-center gap-1.5 py-1 border-b border-default bg-elevated/60 hover:bg-elevated transition-colors text-xs text-muted"
|
||||||
|
@click="metaExpanded = !metaExpanded"
|
||||||
|
>
|
||||||
|
<UIcon :name="metaExpanded ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'" class="size-3.5" />
|
||||||
|
<span>{{ metaExpanded ? 'Ocultar detalles' : 'Ver detalles' }}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Metadata -->
|
<!-- Metadata -->
|
||||||
<div class="flex flex-col gap-3 p-4 sm:px-6 border-b border-default shadow-sm z-10">
|
<div v-show="metaExpanded" class="flex flex-col gap-3 p-4 sm:px-6 border-b border-default shadow-sm z-10">
|
||||||
<div class="flex flex-wrap items-start justify-between gap-2">
|
<div class="flex flex-wrap items-start justify-between gap-2">
|
||||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 min-w-0">
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 min-w-0">
|
||||||
<p v-if="document?.draft" class="text-sm text-highlighted flex items-center gap-1.5 shrink-0">
|
<p v-if="document?.draft" class="text-sm text-highlighted flex items-center gap-1.5 shrink-0">
|
||||||
|
|
@ -779,51 +885,91 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
|
||||||
{{ $t('search.draft') }}
|
{{ $t('search.draft') }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="safeDate()" class="text-sm text-highlighted flex items-center gap-1.5 shrink-0">
|
<p v-if="safeDate()" class="text-sm text-highlighted flex items-center gap-1.5 shrink-0">
|
||||||
<UIcon name="ph:calendar" class="size-4 text-green-600" />
|
<UIcon name="ph:calendar" :class="['size-4', iconColor]" />
|
||||||
{{ safeDate() }}
|
{{ safeDate() }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="document?.activity" class="text-sm flex items-center gap-1.5 text-muted shrink-0">
|
<p v-if="document?.activity" class="text-sm flex items-center gap-1.5 text-muted shrink-0">
|
||||||
<UIcon name="ph:hash" class="size-4 text-green-600 shrink-0" />
|
<UIcon name="ph:hash" :class="['size-4 shrink-0', iconColor]" />
|
||||||
{{ $t('search.publication') }} {{ document.activity }}
|
{{ $t('search.publication') }} {{ document.activity }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="safeLocation()" class="text-sm flex items-center gap-1.5 text-muted min-w-0">
|
<p v-if="safeLocation()" 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" />
|
<UIcon name="ph:map-pin" :class="['size-4 shrink-0', iconColor]" />
|
||||||
<span class="truncate max-w-55">{{ safeLocation() }}</span>
|
<span class="truncate max-w-55">{{ safeLocation() }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<UButton v-if="matchUrl" :to="matchUrl" target="_blank" icon="i-lucide-external-link" label="Ver en sitio"
|
<UButton
|
||||||
color="primary" variant="soft" size="xs" class="shrink-0" />
|
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>
|
||||||
|
|
||||||
<div v-if="fileLinks.length" class="flex flex-wrap gap-2">
|
<!-- file links hidden -->
|
||||||
<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>
|
|
||||||
|
|
||||||
<!-- Buscador interno del documento -->
|
<!-- Buscador interno del documento -->
|
||||||
<div v-if="paragraphs.length" class="flex items-center gap-2">
|
<div v-if="paragraphs.length" v-show="showInternalSearch || !!localQuery" class="flex items-center gap-2">
|
||||||
<UInput v-model="localQuery" icon="i-lucide-search" :placeholder="props.query || 'Buscar en este documento...'"
|
<UInput
|
||||||
class="flex-1 min-w-0" :ui="{ trailing: 'pe-1' }" @keydown="onInputKey">
|
ref="internalSearchRef"
|
||||||
|
v-model="localQuery"
|
||||||
|
icon="i-lucide-search"
|
||||||
|
:placeholder="props.query || 'Buscar en este documento...'"
|
||||||
|
class="flex-1 min-w-0"
|
||||||
|
:ui="{ trailing: 'pe-1' }"
|
||||||
|
@keydown="onInputKey"
|
||||||
|
>
|
||||||
<template #trailing>
|
<template #trailing>
|
||||||
<UButton v-if="localQuery" icon="i-lucide-x" variant="link" aria-label="Limpiar" @click="clearLocalQuery" />
|
<UButton
|
||||||
|
v-if="localQuery"
|
||||||
|
icon="i-lucide-x"
|
||||||
|
variant="link"
|
||||||
|
aria-label="Limpiar"
|
||||||
|
@click="clearLocalQuery"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</UInput>
|
</UInput>
|
||||||
<template v-if="matchElements.length || searchMode === 'local'">
|
<template v-if="matchElements.length || searchMode === 'local'">
|
||||||
<UBadge v-if="searchMode === 'typesense' && matchElements.length" label="Typesense" size="xs" variant="subtle"
|
<UBadge
|
||||||
color="warning" class="shrink-0" />
|
v-if="searchMode === 'typesense' && matchElements.length"
|
||||||
|
label="Typesense"
|
||||||
|
size="xs"
|
||||||
|
variant="subtle"
|
||||||
|
color="warning"
|
||||||
|
class="shrink-0"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
class="text-xs tabular-nums whitespace-nowrap shrink-0 px-2 py-1 rounded-md bg-elevated border border-default"
|
class="text-xs tabular-nums whitespace-nowrap shrink-0 px-2 py-1 rounded-md bg-elevated border border-default"
|
||||||
:class="matchElements.length ? 'text-toned font-medium' : 'text-dimmed'">
|
:class="matchElements.length ? 'text-toned font-medium' : 'text-dimmed'"
|
||||||
|
>
|
||||||
{{ matchElements.length ? `${currentMatchIdx + 1} / ${matchElements.length}` : '0 / 0' }}
|
{{ matchElements.length ? `${currentMatchIdx + 1} / ${matchElements.length}` : '0 / 0' }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
<UTooltip text="Anterior (Shift+Enter)">
|
<UTooltip text="Anterior (Shift+Enter)">
|
||||||
<UButton icon="i-lucide-chevron-up" :disabled="!matchElements.length" aria-label="Anterior"
|
<UButton
|
||||||
color="neutral" variant="ghost" size="sm" @click="prevMatch" />
|
icon="i-lucide-chevron-up"
|
||||||
|
:disabled="!matchElements.length"
|
||||||
|
aria-label="Anterior"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click="prevMatch"
|
||||||
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
<UTooltip text="Siguiente (Enter)">
|
<UTooltip text="Siguiente (Enter)">
|
||||||
<UButton icon="i-lucide-chevron-down" :disabled="!matchElements.length" aria-label="Siguiente"
|
<UButton
|
||||||
color="neutral" variant="ghost" size="sm" @click="nextMatch" />
|
icon="i-lucide-chevron-down"
|
||||||
|
:disabled="!matchElements.length"
|
||||||
|
aria-label="Siguiente"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
@click="nextMatch"
|
||||||
|
/>
|
||||||
</UTooltip>
|
</UTooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -831,95 +977,110 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
|
||||||
</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">
|
||||||
<!-- Cargando párrafos -->
|
<UPage class="max-w-6xl mx-auto py-0 my-0 " @mouseup="handleSelection">
|
||||||
<div v-if="paragraphsLoading" class="flex items-center justify-center gap-2 py-16 text-sm text-muted">
|
<UPageBody class="bg-white p-8 rounded-xl shadow-lg">
|
||||||
<UIcon name="i-lucide-loader-circle" class="size-5 animate-spin" />
|
<UPageHeader
|
||||||
Cargando párrafos...
|
:title="document?.title"
|
||||||
</div>
|
:description="formatSignature(document)"
|
||||||
|
class="py-0 pb-2"
|
||||||
<!-- Todos los párrafos -->
|
>
|
||||||
<div v-else-if="paragraphs.length" ref="paragraphsContainer" class="p-1 sm:p-4 max-w-4xl m-4 sm:mx-auto sm:my-6">
|
<template #headline>
|
||||||
<div class="bg-white rounded-lg shadow-md p-4 pl-2 sm:p-8 sm:pl-4 mb-4 last:mb-0">
|
<div class="flex w-full justify-between" v-if="paragraphs.length>0">
|
||||||
<div v-for="(hit, idx) in paragraphs" :key="idx" :data-paragraph-number="hit.document.number">
|
<UBadge variant="outline" color="neutral" :class="document?.type=='activities' ? 'bg-carpagreen/20' : 'bg-carpablue/20'">{{ document?.locale.toUpperCase() }}-{{ document?.code }}</UBadge>
|
||||||
<div class="grid grid-cols-1fr items-start gap-2 mb-2" :class="(showParagraphNumbers && 'grid-cols-[32px_1fr]')">
|
<div class="text-carpared font-semibold" v-if="document?.draft"><UIcon name="ph-file-dashed" class="mr-1" />{{ $t('ui.draft') }}</div>
|
||||||
<div v-if="showParagraphNumbers" class="w-full select-none cursor-pointer flex justify-end">
|
|
||||||
<UPopover :content="{
|
|
||||||
align: 'right',
|
|
||||||
side: 'top',
|
|
||||||
sideOffset: -10
|
|
||||||
}" placement="top" :ui="{ maxWidth: 'max-w-lg' }">
|
|
||||||
<UBadge
|
|
||||||
v-if="hit.document.number"
|
|
||||||
:label="`${hit.document.number}`"
|
|
||||||
size="md"
|
|
||||||
variant="ghost"
|
|
||||||
class="text-gray-300 hover:text-white"
|
|
||||||
:class="(hit.document.type=='activities'?'hover:bg-carpagreen':'hover:bg-carpablue')"
|
|
||||||
/>
|
|
||||||
<template #content>
|
|
||||||
<UFieldGroup size="sm" orientation="horizontal" class="bg-white p-0 rounded-md shadow-md">
|
|
||||||
<UPopover>
|
|
||||||
<UButton
|
|
||||||
v-if="hit.document.text"
|
|
||||||
icon="ph-note-pencil"
|
|
||||||
variant="ghost"
|
|
||||||
color="neutral"
|
|
||||||
size="lg"
|
|
||||||
@click="addNote(hit.document.id)"
|
|
||||||
></UButton>
|
|
||||||
|
|
||||||
<template #content>
|
|
||||||
<UEditor />
|
|
||||||
</template>
|
|
||||||
</UPopover>
|
|
||||||
|
|
||||||
<UPopover>
|
|
||||||
<UButton
|
|
||||||
v-if="hit.document.text"
|
|
||||||
icon="ph-highlighter"
|
|
||||||
variant="ghost"
|
|
||||||
color="neutral"
|
|
||||||
size="lg"
|
|
||||||
@click="highlightParagraph(hit.document.id)"
|
|
||||||
></UButton>
|
|
||||||
|
|
||||||
<template #content>
|
|
||||||
<UColorPicker />
|
|
||||||
</template>
|
|
||||||
</UPopover>
|
|
||||||
<UButton
|
|
||||||
v-if="hit.document.text"
|
|
||||||
icon="ph-copy"
|
|
||||||
variant="ghost"
|
|
||||||
color="neutral"
|
|
||||||
size="lg"
|
|
||||||
@click="copyToClipboard(hit.document.text, hit.document.type)"
|
|
||||||
></UButton>
|
|
||||||
</UFieldGroup>
|
|
||||||
</template>
|
|
||||||
</UPopover>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
</template>
|
||||||
<div
|
</UPageHeader>
|
||||||
class="paragraph-html text-sm leading-relaxed text-gray-800 dark:text-gray-200"
|
<div v-if="paragraphsLoading" class="flex items-center justify-center gap-2 py-16 text-sm text-muted">
|
||||||
v-html="hit.document.raw"
|
<UIcon name="i-lucide-loader-circle" class="size-5 animate-spin" />
|
||||||
/>
|
Cargando párrafos...
|
||||||
|
</div>
|
||||||
|
<div v-else-if="paragraphs.length" ref="paragraphsContainer">
|
||||||
|
<div class="">
|
||||||
|
<div v-for="(hit, idx) in paragraphs" :key="idx" :data-paragraph-number="hit.document.number">
|
||||||
|
<div class="grid grid-cols-1fr items-start gap-2 mb-2" :class="(showParagraphNumbers && 'grid-cols-[20px_1fr]')">
|
||||||
|
<div v-if="showParagraphNumbers" class="w-full select-none cursor-pointer flex justify-end">
|
||||||
|
<UBadge
|
||||||
|
v-if="hit.document.number"
|
||||||
|
:label="`${hit.document.number}`"
|
||||||
|
size="md"
|
||||||
|
variant="link"
|
||||||
|
class="text-gray-300 font-bold"
|
||||||
|
:class="(hit.document.type=='activities'?'hover:text-carpagreen':'hover:text-carpablue')"
|
||||||
|
@click="isPopOverOpen = !isPopOverOpen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="">
|
||||||
|
<div
|
||||||
|
class="paragraph-html text-sm leading-relaxed text-gray-800 dark:text-gray-200"
|
||||||
|
v-html="hit.document.html || hit.document.text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sin contenido -->
|
<!-- Sin contenido -->
|
||||||
<div v-else class="flex flex-col items-center justify-center gap-2 py-16 text-dimmed text-sm">
|
<div v-else class="flex flex-col items-center justify-center gap-2 py-16 text-dimmed text-sm">
|
||||||
<UIcon name="i-lucide-file-x" class="size-10" />
|
<UIcon name="i-lucide-file-x" class="size-10" />
|
||||||
<p>No hay contenido disponible para este documento.</p>
|
<p>No hay contenido disponible para este documento.</p>
|
||||||
<p v-if="matchUrl">
|
<p v-if="matchUrl">
|
||||||
Puedes
|
Puedes
|
||||||
<ULink :to="matchUrl" target="_blank" class="text-primary">verlo en el sitio</ULink>.
|
<ULink :to="matchUrl" target="_blank" class="text-primary">verlo en el sitio</ULink>.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</UPageBody>
|
||||||
|
|
||||||
|
</UPage>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tooltip de selección -->
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="selectionTooltip"
|
||||||
|
class="fixed z-9999 pointer-events-none"
|
||||||
|
:style="{ left: selectionTooltip.x + 'px', top: (selectionTooltip.y - 12) + 'px' }"
|
||||||
|
>
|
||||||
|
<Transition name="tooltip-pop">
|
||||||
|
<div
|
||||||
|
v-if="isPopOverOpen"
|
||||||
|
class="p-2 selection-tooltip flex items-center bg-white rounded-xl shadow-2xl overflow-hidden select-none pointer-events-auto"
|
||||||
|
@mousedown.prevent
|
||||||
|
>
|
||||||
|
<!-- <UButton
|
||||||
|
variant="soft"
|
||||||
|
class="group flex flex-col items-center gap-1 px-4 py-2.5 text-blue-300 hover:bg-white/10 active:bg-white/20 transition-colors"
|
||||||
|
aria-label="Crear nota"
|
||||||
|
@click="addNote()"
|
||||||
|
>
|
||||||
|
<UIcon name="ph-note-pencil" class="size-5" />
|
||||||
|
<span class="group-hover:text-black text-[9px] font-semibold leading-none">Nota</span>
|
||||||
|
</UButton>
|
||||||
|
<div class="w-px self-stretch bg-white/10" /> -->
|
||||||
|
<!-- <UButton
|
||||||
|
variant="soft"
|
||||||
|
class="group flex flex-col items-center gap-1 px-4 py-2.5 text-green-400 hover:bg-white/10 active:bg-white/20 transition-colors"
|
||||||
|
aria-label="Resaltar"
|
||||||
|
@click="highlightParagraph()"
|
||||||
|
>
|
||||||
|
<UIcon name="ph-highlighter" class="size-5" />
|
||||||
|
<span class="group-hover:text-black text-[9px] font-semibold leading-none">Resaltar</span>
|
||||||
|
</UButton> -->
|
||||||
|
<div class="w-px self-stretch bg-white/10" />
|
||||||
|
<UButton
|
||||||
|
variant="soft"
|
||||||
|
class="cursor-pointer group flex flex-col items-center gap-1 px-4 py-2.5 text-carpared hover:bg-white/10 active:bg-white/20 transition-colors"
|
||||||
|
:aria-label="$t('ui.copy')"
|
||||||
|
@click="copyToClipboard(props.document)"
|
||||||
|
>
|
||||||
|
<UIcon name="ph-clipboard-text" class="size-7" />
|
||||||
|
<span class="group-hover:text-black text-xs font-semibold leading-none">{{ $t('ui.copy') }}</span>
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -931,4 +1092,21 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
|
||||||
.paragraph-html :deep(p:last-child) {
|
.paragraph-html :deep(p:last-child) {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Tooltip de selección de texto */
|
||||||
|
.selection-tooltip {
|
||||||
|
transform: translateX(-50%) translateY(-100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip-pop-enter-active {
|
||||||
|
transition: transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.12s ease;
|
||||||
|
}
|
||||||
|
.tooltip-pop-leave-active {
|
||||||
|
transition: transform 0.12s ease-in, opacity 0.1s ease;
|
||||||
|
}
|
||||||
|
.tooltip-pop-enter-from,
|
||||||
|
.tooltip-pop-leave-to {
|
||||||
|
transform: translateX(-50%) translateY(calc(-100% + 8px));
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
@ -18,8 +18,11 @@ interface EntrelineaDoc {
|
||||||
link?: string
|
link?: string
|
||||||
locale?: string
|
locale?: string
|
||||||
origin?: string
|
origin?: string
|
||||||
|
filter?: string
|
||||||
page?: number | string
|
page?: number | string
|
||||||
text?: string
|
text?: string
|
||||||
|
html?: string
|
||||||
|
draft?: boolean
|
||||||
studies?: Study[]
|
studies?: Study[]
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
@ -37,6 +40,8 @@ const showStudies = ref(false)
|
||||||
// Volver al detalle cuando cambia el documento
|
// Volver al detalle cuando cambia el documento
|
||||||
watch(() => props.document?.id, () => { showStudies.value = false })
|
watch(() => props.document?.id, () => { showStudies.value = false })
|
||||||
|
|
||||||
|
const { unlocked } = useDevMode()
|
||||||
|
|
||||||
const imageUrl = computed<string | null>(() =>
|
const imageUrl = computed<string | null>(() =>
|
||||||
getEntrelineaImageUrl(props.document?.image, 'detail')
|
getEntrelineaImageUrl(props.document?.image, 'detail')
|
||||||
)
|
)
|
||||||
|
|
@ -48,6 +53,7 @@ const title = computed(() =>
|
||||||
)
|
)
|
||||||
|
|
||||||
function formatEntrelineaText(html: string): string {
|
function formatEntrelineaText(html: string): string {
|
||||||
|
return html;
|
||||||
if (!html) return ''
|
if (!html) return ''
|
||||||
|
|
||||||
const sPrefix = "font-family:'Times New Roman',serif;font-style:italic;color:#c00000"
|
const sPrefix = "font-family:'Times New Roman',serif;font-style:italic;color:#c00000"
|
||||||
|
|
@ -78,7 +84,7 @@ function formatEntrelineaText(html: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
const bodyHtml = computed<string>(() =>
|
const bodyHtml = computed<string>(() =>
|
||||||
formatEntrelineaText(props.highlightedText || props.document?.text || '')
|
formatEntrelineaText(props.document?.html || props.document?.text || '')
|
||||||
)
|
)
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
/* -------------------------------------------------------------------------- */
|
||||||
|
|
@ -91,7 +97,7 @@ const toast = useToast()
|
||||||
|
|
||||||
function toSearchHit(doc: EntrelineaDoc): SearchHit {
|
function toSearchHit(doc: EntrelineaDoc): SearchHit {
|
||||||
const id = doc.id || ''
|
const id = doc.id || ''
|
||||||
return { _id: id, id, title: id || 'Entrelínea', body: doc.text, ...doc }
|
return { _id: id, id, title: doc.text?.slice(0, 100) || 'Entrelínea', body: doc.text, ...doc }
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|
@ -112,9 +118,10 @@ function onToggleFavorite() {
|
||||||
if (!props.collection || !props.document?.id) return
|
if (!props.collection || !props.document?.id) return
|
||||||
const wasFav = isFav.value
|
const wasFav = isFav.value
|
||||||
favorites.toggle(props.collection, toSearchHit(props.document))
|
favorites.toggle(props.collection, toSearchHit(props.document))
|
||||||
|
console.log("text slice", props.document.text?.slice(0, 100))
|
||||||
toast.add({
|
toast.add({
|
||||||
title: wasFav ? 'Eliminado de tu lista' : 'Guardado en tu lista',
|
title: 'Entrelínea',
|
||||||
description: props.document.id,
|
description: props.document.text?.slice(0, 100) || props.document.id,
|
||||||
icon: wasFav ? 'i-lucide-bookmark-x' : 'i-lucide-bookmark-check',
|
icon: wasFav ? 'i-lucide-bookmark-x' : 'i-lucide-bookmark-check',
|
||||||
color: wasFav ? 'neutral' : 'primary',
|
color: wasFav ? 'neutral' : 'primary',
|
||||||
duration: 1800
|
duration: 1800
|
||||||
|
|
@ -264,8 +271,6 @@ function onLightboxTouchStart(e: TouchEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onLightboxTouchMove(e: TouchEvent) {
|
function onLightboxTouchMove(e: TouchEvent) {
|
||||||
// Prevent page scroll while interacting inside the lightbox
|
|
||||||
e.preventDefault()
|
|
||||||
if (e.touches.length === 1 && lightboxScale.value > 1) {
|
if (e.touches.length === 1 && lightboxScale.value > 1) {
|
||||||
const dx = e.touches[0].clientX - lbLastTouchX
|
const dx = e.touches[0].clientX - lbLastTouchX
|
||||||
const dy = e.touches[0].clientY - lbLastTouchY
|
const dy = e.touches[0].clientY - lbLastTouchY
|
||||||
|
|
@ -334,44 +339,8 @@ const lightboxImgStyle = computed(() => ({
|
||||||
/>
|
/>
|
||||||
</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 wrap-anywhere"
|
|
||||||
:title="title"
|
|
||||||
>
|
|
||||||
{{ title }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #right>
|
<template #right>
|
||||||
<template v-if="!showStudies">
|
<template v-if="!showStudies">
|
||||||
<UTooltip
|
|
||||||
v-if="document.studies?.length"
|
|
||||||
:text="`Estudios relacionados (${document.studies.length})`"
|
|
||||||
>
|
|
||||||
<UChip
|
|
||||||
:text="document.studies.length"
|
|
||||||
color="primary"
|
|
||||||
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'">
|
<UTooltip v-if="collection" :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'"
|
||||||
|
|
@ -398,36 +367,22 @@ const lightboxImgStyle = computed(() => ({
|
||||||
size="xs"
|
size="xs"
|
||||||
class="shrink-0"
|
class="shrink-0"
|
||||||
/>
|
/>
|
||||||
|
<UBadge
|
||||||
|
v-if="document.filter"
|
||||||
|
:label="String(document.filter)"
|
||||||
|
color="neutral"
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
class="shrink-0 uppercase"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="origin && origin !== document.id"
|
v-if="origin"
|
||||||
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]"
|
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"
|
:title="origin"
|
||||||
>
|
>
|
||||||
{{ origin }}
|
{{ origin }}
|
||||||
</span>
|
</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>
|
||||||
|
|
||||||
<!-- ------------------------------------------------------------------ -->
|
<!-- ------------------------------------------------------------------ -->
|
||||||
|
|
@ -437,13 +392,13 @@ const lightboxImgStyle = computed(() => ({
|
||||||
<div class="p-4 sm:p-6 pb-10 flex flex-col gap-6">
|
<div class="p-4 sm:p-6 pb-10 flex flex-col gap-6">
|
||||||
|
|
||||||
<!-- Imagen: mobile toca para abrir lightbox, desktop zoom al hover -->
|
<!-- Imagen: mobile toca para abrir lightbox, desktop zoom al hover -->
|
||||||
<template v-if="imageUrl">
|
<template v-if="imageUrl && (!document.draft || unlocked)">
|
||||||
<!-- Mobile: imagen con indicador de zoom táctil -->
|
<!-- Mobile: imagen con indicador de zoom táctil -->
|
||||||
<div
|
<div
|
||||||
class="lg:hidden rounded-xl overflow-hidden border border-default shadow-sm bg-elevated relative cursor-pointer"
|
class="lg:hidden h-52 rounded-xl overflow-hidden border border-default shadow-sm bg-elevated relative cursor-pointer flex items-center justify-center"
|
||||||
@click="openLightbox"
|
@click="openLightbox"
|
||||||
>
|
>
|
||||||
<img :src="imageUrl" :alt="title" loading="lazy" class="w-full h-auto select-none">
|
<img :src="imageUrl" :alt="title" loading="lazy" class="w-full h-full object-contain select-none">
|
||||||
<div class="absolute bottom-2 right-2 bg-black/50 rounded-full p-1.5 pointer-events-none">
|
<div class="absolute bottom-2 right-2 bg-black/50 rounded-full p-1.5 pointer-events-none">
|
||||||
<UIcon name="i-lucide-zoom-in" class="size-4 text-white" />
|
<UIcon name="i-lucide-zoom-in" class="size-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -452,7 +407,7 @@ const lightboxImgStyle = computed(() => ({
|
||||||
<!-- Desktop: hover zoom con lente -->
|
<!-- Desktop: hover zoom con lente -->
|
||||||
<div
|
<div
|
||||||
ref="zoomBoxRef"
|
ref="zoomBoxRef"
|
||||||
class="hidden lg:flex rounded-xl bg-elevated border border-default shadow-sm p-4 items-center justify-center relative cursor-zoom-in select-none min-h-48"
|
class="hidden lg:flex h-52 rounded-xl bg-elevated border border-default shadow-sm p-4 items-center justify-center relative cursor-zoom-in select-none"
|
||||||
@mouseenter="onZoomEnter"
|
@mouseenter="onZoomEnter"
|
||||||
@mouseleave="onZoomLeave"
|
@mouseleave="onZoomLeave"
|
||||||
@mousemove="onZoomMove"
|
@mousemove="onZoomMove"
|
||||||
|
|
@ -476,13 +431,13 @@ const lightboxImgStyle = computed(() => ({
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Sin imagen -->
|
<!-- Sin imagen / Borrador -->
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
class="rounded-xl 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>Imagen no disponible</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Texto -->
|
<!-- Texto -->
|
||||||
|
|
@ -565,31 +520,47 @@ const lightboxImgStyle = computed(() => ({
|
||||||
/>
|
/>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
|
|
||||||
<!-- Lightbox mobile con pinch-to-zoom -->
|
<!-- Popup móvil: imagen con pinch-to-zoom -->
|
||||||
<Teleport to="body">
|
<UModal
|
||||||
<div
|
v-model:open="lightboxOpen"
|
||||||
v-if="lightboxOpen"
|
:ui="{
|
||||||
class="lg:hidden fixed inset-0 bg-black z-[9999] flex items-center justify-center"
|
overlay: 'bg-black/90',
|
||||||
@touchstart.passive="onLightboxTouchStart"
|
content: 'bg-black text-white max-w-[95vw] sm:max-w-lg',
|
||||||
@touchmove.prevent="onLightboxTouchMove"
|
header: 'py-2 px-3 border-b-0',
|
||||||
@touchend="onLightboxTouchEnd"
|
body: 'p-0',
|
||||||
@click.self="closeLightbox"
|
}"
|
||||||
>
|
>
|
||||||
<button
|
<template #header>
|
||||||
class="absolute top-4 right-4 z-10 text-white bg-black/60 rounded-full p-2"
|
<div class="w-full flex justify-end">
|
||||||
@click="closeLightbox"
|
<UButton
|
||||||
|
icon="i-lucide-x"
|
||||||
|
size="sm"
|
||||||
|
color="neutral"
|
||||||
|
variant="ghost"
|
||||||
|
class="text-white hover:bg-white/20"
|
||||||
|
@click="closeLightbox"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #body>
|
||||||
|
<div
|
||||||
|
class="touch-none flex items-center justify-center overflow-hidden"
|
||||||
|
style="height: 70dvh"
|
||||||
|
@touchstart.passive="onLightboxTouchStart"
|
||||||
|
@touchmove.passive="onLightboxTouchMove"
|
||||||
|
@touchend.passive="onLightboxTouchEnd"
|
||||||
>
|
>
|
||||||
<UIcon name="i-lucide-x" class="size-6" />
|
<img
|
||||||
</button>
|
v-if="imageUrl"
|
||||||
<img
|
:src="imageUrl"
|
||||||
:src="imageUrl!"
|
:alt="title"
|
||||||
:alt="title"
|
class="w-full h-full object-contain select-none"
|
||||||
class="max-w-full max-h-full object-contain select-none"
|
:style="lightboxImgStyle"
|
||||||
:style="lightboxImgStyle"
|
draggable="false"
|
||||||
draggable="false"
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</Teleport>
|
</UModal>
|
||||||
|
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -434,21 +434,7 @@ function onInputKey(e: KeyboardEvent) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Fila 2: archivos adjuntos -->
|
<!-- file links hidden -->
|
||||||
<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 -->
|
<!-- Fila 3: búsqueda local en el documento -->
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,757 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { breakpointsTailwind, useDebounce } from '@vueuse/core'
|
||||||
|
import PublicationDetail from '~/components/PublicationDetail.vue'
|
||||||
|
import { useSettingsStore } from '~/stores/settings'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
paragraphsCollection: string
|
||||||
|
mainCollection: string
|
||||||
|
groupByField: string
|
||||||
|
favoritesCollection: string
|
||||||
|
panelId: string
|
||||||
|
navTitleKey: string
|
||||||
|
accentColor: 'green' | 'blue'
|
||||||
|
emptyDetailText: string
|
||||||
|
showDraft?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
showDraft: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const QUERY_BY = 'text'
|
||||||
|
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
const t = $i18n.t
|
||||||
|
const { locale } = useI18n()
|
||||||
|
|
||||||
|
const filterBy = computed(() => `locale:=${locale.value}`)
|
||||||
|
const REQUEST_TIMEOUT_MS = 15000
|
||||||
|
|
||||||
|
const settings = useSettingsStore()
|
||||||
|
|
||||||
|
// ── Restaurar estado desde URL antes de crear los refs ─────────────────────
|
||||||
|
const { query: q0, page: p0, scroll: s0, selectedId: sid0 } = useSearchUrlState()
|
||||||
|
|
||||||
|
const query = ref(q0)
|
||||||
|
const debouncedQuery = useDebounce(query, 150)
|
||||||
|
const loading = ref(false)
|
||||||
|
const loadingMore = ref(false)
|
||||||
|
const errorMsg = ref<string | null>(null)
|
||||||
|
|
||||||
|
// ---- Types ----------------------------------------------------------------
|
||||||
|
|
||||||
|
interface ParagraphDoc {
|
||||||
|
id?: string
|
||||||
|
document_id: string
|
||||||
|
text: string
|
||||||
|
number: number
|
||||||
|
locale: string
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocMeta {
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
date?: string
|
||||||
|
timestamp?: number
|
||||||
|
place?: string
|
||||||
|
city?: string
|
||||||
|
state?: string
|
||||||
|
country?: string
|
||||||
|
type?: string
|
||||||
|
slug?: string
|
||||||
|
draft?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DocumentDoc extends DocMeta {
|
||||||
|
code: string
|
||||||
|
locale: string
|
||||||
|
files?: {
|
||||||
|
youtube?: string
|
||||||
|
video?: string
|
||||||
|
audio?: string
|
||||||
|
booklet?: string
|
||||||
|
simple?: string
|
||||||
|
}
|
||||||
|
body?: string
|
||||||
|
[key: string]: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TypesenseHighlight {
|
||||||
|
field?: string
|
||||||
|
snippet?: string
|
||||||
|
value?: string
|
||||||
|
matched_tokens?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TypesenseParagraphHit {
|
||||||
|
document: ParagraphDoc
|
||||||
|
highlights?: TypesenseHighlight[]
|
||||||
|
highlight?: Record<string, { snippet?: string, value?: string }>
|
||||||
|
text_match?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TypesenseGroupedHit {
|
||||||
|
groupKey: string[]
|
||||||
|
hits: TypesenseParagraphHit[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TypesenseSearchResponse {
|
||||||
|
found: number
|
||||||
|
groupedHits?: TypesenseGroupedHit[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SearchGroup {
|
||||||
|
docId: string
|
||||||
|
firstHit: TypesenseParagraphHit
|
||||||
|
allHits: TypesenseParagraphHit[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BrowseItem {
|
||||||
|
docId: string
|
||||||
|
meta: DocMeta
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DisplayGroup {
|
||||||
|
docId: string
|
||||||
|
meta: DocMeta | undefined
|
||||||
|
firstHit: TypesenseParagraphHit | null
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Colors ----------------------------------------------------------------
|
||||||
|
|
||||||
|
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',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---- State ----------------------------------------------------------------
|
||||||
|
|
||||||
|
const exactSearch = ref(false)
|
||||||
|
|
||||||
|
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
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
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()
|
||||||
|
|
||||||
|
// ---- 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(',')}]`,
|
||||||
|
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 multi = await documentsApi.multiSearch({
|
||||||
|
multiSearchParameters: {},
|
||||||
|
multiSearchSearchesParameter: {
|
||||||
|
searches: [{
|
||||||
|
collection: props.paragraphsCollection,
|
||||||
|
q: exactSearch.value && q ? `"${q}"` : q || '*',
|
||||||
|
queryBy: QUERY_BY,
|
||||||
|
filterBy: filterBy.value,
|
||||||
|
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,
|
||||||
|
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)`
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('Browse result', multi)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listContainer = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
function onListScroll() {
|
||||||
|
if (settings.paginationType !== 'infinite_scroll') return
|
||||||
|
const el = listContainer.value
|
||||||
|
if (!el) return
|
||||||
|
if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
|
||||||
|
if (!debouncedQuery.value.trim()) {
|
||||||
|
if (hasMoreBrowse.value && !loadingMore.value && !loading.value) runBrowse(browsePage.value, true)
|
||||||
|
} else {
|
||||||
|
if (hasMoreVisible.value) visibleGroupCount.value += 10
|
||||||
|
else if (hasMore.value && !loadingMore.value && !loading.value) loadMore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function retry() {
|
||||||
|
if (!query.value.trim()) runBrowse(activePage.value, false)
|
||||||
|
else runSearch(query.value, activePage.value, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => { if (timeoutId) clearTimeout(timeoutId) })
|
||||||
|
|
||||||
|
watch(debouncedQuery, (q) => {
|
||||||
|
activePage.value = 1
|
||||||
|
if (!q.trim()) {
|
||||||
|
groupedHits.value = []; total.value = 0; currentPage.value = 1; visibleGroupCount.value = 10
|
||||||
|
browseItems.value = []; browseTotal.value = 0; browsePage.value = 1
|
||||||
|
runBrowse(1, false)
|
||||||
|
} else {
|
||||||
|
browseItems.value = []; browseTotal.value = 0; browsePage.value = 1
|
||||||
|
groupedHits.value = []; total.value = 0; currentPage.value = 1; visibleGroupCount.value = 10
|
||||||
|
runSearch(q, 1, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(exactSearch, () => {
|
||||||
|
if (query.value.trim()) runSearch(query.value, 1, false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---- Selección y carga del detalle ----------------------------------------
|
||||||
|
|
||||||
|
const selectedDocId = ref<string | null>(null)
|
||||||
|
const selectedDocument = ref<DocumentDoc | null>(null)
|
||||||
|
const documentLoading = ref(false)
|
||||||
|
const selectedParagraphs = ref<TypesenseParagraphHit[]>([])
|
||||||
|
const paragraphsLoading = ref(false)
|
||||||
|
const selectedHit = ref<TypesenseParagraphHit | null>(null)
|
||||||
|
const selectedMatchingHits = ref<TypesenseParagraphHit[]>([])
|
||||||
|
|
||||||
|
async function fetchDocumentWithParagraphs(docId: string) {
|
||||||
|
documentLoading.value = true
|
||||||
|
paragraphsLoading.value = true
|
||||||
|
selectedDocument.value = null
|
||||||
|
selectedParagraphs.value = []
|
||||||
|
try {
|
||||||
|
const res = await documentsApi.multiSearch({
|
||||||
|
multiSearchParameters: {},
|
||||||
|
multiSearchSearchesParameter: {
|
||||||
|
searches: [{
|
||||||
|
collection: props.mainCollection,
|
||||||
|
q: '*',
|
||||||
|
queryBy: 'title',
|
||||||
|
filterBy: `id:=${docId} && $${props.paragraphsCollection}(id: *)`,
|
||||||
|
includeFields: `*, $${props.paragraphsCollection}(*)`
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const hit = (res?.results?.[0] as { hits?: Array<{ document: Record<string, unknown> }> })?.hits?.[0]
|
||||||
|
if (hit) {
|
||||||
|
const docRaw = { ...hit.document }
|
||||||
|
const rawParagraphs = (docRaw[props.paragraphsCollection] as ParagraphDoc[] | undefined) ?? []
|
||||||
|
delete docRaw[props.paragraphsCollection]
|
||||||
|
selectedDocument.value = docRaw as unknown as DocumentDoc
|
||||||
|
selectedParagraphs.value = [...rawParagraphs]
|
||||||
|
.sort((a, b) => (a.number ?? 0) - (b.number ?? 0))
|
||||||
|
.map(p => ({ document: p }))
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching document with paragraphs', err)
|
||||||
|
selectedDocument.value = null
|
||||||
|
selectedParagraphs.value = []
|
||||||
|
} finally {
|
||||||
|
documentLoading.value = false
|
||||||
|
paragraphsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectGroup(group: DisplayGroup) {
|
||||||
|
selectedDocId.value = group.docId
|
||||||
|
selectedHit.value = group.firstHit
|
||||||
|
selectedMatchingHits.value = groupedHits.value.find(g => g.docId === group.docId)?.allHits ?? []
|
||||||
|
fetchDocumentWithParagraphs(group.docId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPanelOpen = computed({
|
||||||
|
get() { return !!selectedDocId.value },
|
||||||
|
set(v: boolean) {
|
||||||
|
if (!v) {
|
||||||
|
selectedDocId.value = null
|
||||||
|
selectedDocument.value = null
|
||||||
|
selectedParagraphs.value = []
|
||||||
|
selectedHit.value = null
|
||||||
|
selectedMatchingHits.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(groupedHits, () => {
|
||||||
|
if (!selectedDocId.value || !debouncedQuery.value.trim()) return
|
||||||
|
if (!groupedHits.value.find(g => g.docId === selectedDocId.value)) {
|
||||||
|
selectedDocId.value = null
|
||||||
|
selectedDocument.value = null
|
||||||
|
selectedParagraphs.value = []
|
||||||
|
selectedHit.value = null
|
||||||
|
selectedMatchingHits.value = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const selectedId = computed(() => selectedDocId.value)
|
||||||
|
|
||||||
|
useSearchUrlSync({ query, page: activePage, selectedId, scrollEl: listContainer })
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (q0.trim()) await runSearch(q0, p0, false)
|
||||||
|
else await runBrowse(p0, false)
|
||||||
|
|
||||||
|
restoreScrollPosition(listContainer.value, s0)
|
||||||
|
|
||||||
|
if (sid0) {
|
||||||
|
const group = displayGroups.value.find(g => g.docId === sid0)
|
||||||
|
if (group) selectGroup(group)
|
||||||
|
else {
|
||||||
|
selectedDocId.value = sid0
|
||||||
|
fetchDocumentWithParagraphs(sid0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const breakpoints = useBreakpoints(breakpointsTailwind)
|
||||||
|
const isMobile = breakpoints.smaller('lg')
|
||||||
|
|
||||||
|
useDetailHistory(isPanelOpen, isMobile)
|
||||||
|
|
||||||
|
// ---- Helpers de presentación ----------------------------------------------
|
||||||
|
|
||||||
|
function highlightedFor(hit: TypesenseParagraphHit, field: string): string | null {
|
||||||
|
const fromArr = hit.highlights?.find(h => h.field === field)
|
||||||
|
if (fromArr?.snippet) return fromArr.snippet
|
||||||
|
if (fromArr?.value) return fromArr.value
|
||||||
|
const fromObj = hit.highlight?.[field]
|
||||||
|
if (fromObj?.snippet) return fromObj.snippet
|
||||||
|
if (fromObj?.value) return fromObj.value
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function metaDate(meta: DocMeta | undefined): string {
|
||||||
|
if (!meta) return ''
|
||||||
|
const ts = meta.timestamp || (meta.date ? Math.floor(new Date(meta.date).getTime() / 1000) : null)
|
||||||
|
if (!ts) return meta.date || ''
|
||||||
|
return formatDate(ts)
|
||||||
|
}
|
||||||
|
|
||||||
|
function metaLocation(meta: DocMeta | undefined): string {
|
||||||
|
if (!meta) return ''
|
||||||
|
return formatLocation({
|
||||||
|
id: meta.id, date: meta.timestamp ?? 0, slug: meta.slug ?? '',
|
||||||
|
type: meta.type ?? '', place: meta.place ?? '', city: meta.city ?? '',
|
||||||
|
state: meta.state ?? '', country: meta.country ?? '', thumbnail: ''
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardPanel
|
||||||
|
:id="panelId"
|
||||||
|
:default-size="32"
|
||||||
|
:min-size="24"
|
||||||
|
:max-size="45"
|
||||||
|
resizable
|
||||||
|
>
|
||||||
|
<UDashboardNavbar :title="t(navTitleKey)">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse :ui="{
|
||||||
|
base: 'collapse-sidebar-icon'
|
||||||
|
}" />
|
||||||
|
</template>
|
||||||
|
<template #trailing>
|
||||||
|
<UBadge :label="displayTotal" variant="subtle" :ui="{
|
||||||
|
base: 'total-results'
|
||||||
|
}" />
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<div class="px-4 sm:px-6 py-3 border-b border-default flex items-center gap-2" id="inputField">
|
||||||
|
<UInput
|
||||||
|
v-model="query"
|
||||||
|
icon="i-lucide-search"
|
||||||
|
:placeholder="t('search.placeholder')"
|
||||||
|
:loading="loading"
|
||||||
|
size="md"
|
||||||
|
class="flex-1 min-w-0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="flex rounded-full p-0.5 shrink-0 transition-colors duration-200"
|
||||||
|
:class="exactSearch ? 'bg-primary' : 'bg-gray-200 dark:bg-gray-700'"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="px-2.5 py-0.5 rounded-full text-xs transition-all duration-200 whitespace-nowrap"
|
||||||
|
:class="!exactSearch ? 'bg-white dark:bg-gray-900 text-gray-900 dark:text-white font-semibold shadow-sm' : 'text-white/40 font-normal'"
|
||||||
|
@click.stop="exactSearch = false"
|
||||||
|
>{{ t('search.word') }}</button>
|
||||||
|
<button
|
||||||
|
class="px-2.5 py-0.5 rounded-full text-xs transition-all duration-200 whitespace-nowrap"
|
||||||
|
:class="exactSearch ? 'bg-white text-primary font-semibold shadow-sm' : 'text-gray-400 dark:text-gray-400 font-normal'"
|
||||||
|
@click.stop="exactSearch = true"
|
||||||
|
>{{ t('search.phrase') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-if="errorMsg"
|
||||||
|
:title="errorMsg"
|
||||||
|
color="error"
|
||||||
|
variant="subtle"
|
||||||
|
icon="i-lucide-triangle-alert"
|
||||||
|
class="mx-4 my-2"
|
||||||
|
:actions="[{ label: 'Reintentar', color: 'neutral', variant: 'outline', onClick: retry }]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div ref="listContainer" class="overflow-y-auto divide-y divide-default flex-1" @scroll="onListScroll">
|
||||||
|
<div
|
||||||
|
v-if="loading && !displayGroups.length"
|
||||||
|
class="flex items-center justify-center gap-2 py-16 text-sm text-muted"
|
||||||
|
>
|
||||||
|
<UIcon name="i-lucide-loader-circle" class="size-4 animate-spin" />
|
||||||
|
Buscando...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="!displayGroups.length"
|
||||||
|
class="flex flex-col items-center justify-center gap-2 py-16 text-dimmed text-sm"
|
||||||
|
>
|
||||||
|
<UIcon name="i-lucide-inbox" class="size-10" />
|
||||||
|
<p>{{ query ? `Sin coincidencias para "${query}"` : 'Sin resultados' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-for="group in displayGroups"
|
||||||
|
:key="group.docId"
|
||||||
|
class="p-4 sm:px-6 text-sm cursor-pointer border-b-2 transition-colors"
|
||||||
|
:class="selectedDocId === group.docId ? colors.selectedItem : colors.hoverItem"
|
||||||
|
@click="selectGroup(group)"
|
||||||
|
>
|
||||||
|
<div class="mb-1">
|
||||||
|
<p class="text-sm font-semibold line-clamp-2 text-highlighted">
|
||||||
|
<UTooltip
|
||||||
|
v-if="showDraft && group.meta?.draft"
|
||||||
|
:text="$t('search.draft')"
|
||||||
|
color="error"
|
||||||
|
>
|
||||||
|
<UIcon name="ph-file-dashed" class="bg-carpared" />
|
||||||
|
</UTooltip>
|
||||||
|
{{ group.meta?.title || group.docId }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 text-xs mb-2 text-muted justify-between">
|
||||||
|
<span v-if="metaDate(group.meta)" class="flex items-center gap-1">
|
||||||
|
<UIcon name="ph:calendar" :class="['size-4', colors.icon]" />
|
||||||
|
{{ metaDate(group.meta) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="metaLocation(group.meta)" class="flex items-center gap-1 truncate">
|
||||||
|
<UIcon name="ph:map-pin" :class="['size-4 shrink-0', colors.icon]" />
|
||||||
|
<span class="truncate">{{ metaLocation(group.meta) }}</span>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="group.firstHit"
|
||||||
|
class="snippet-html text-sm text-dimmed"
|
||||||
|
v-html="highlightedFor(group.firstHit, 'text') || group.firstHit.document.text"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="settings.paginationType === 'infinite_scroll' && loadingMore"
|
||||||
|
class="flex items-center justify-center gap-2 py-4 text-sm text-muted"
|
||||||
|
>
|
||||||
|
<UIcon name="i-lucide-loader-circle" class="size-4 animate-spin" />
|
||||||
|
Cargando más...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="settings.paginationType === 'infinite_scroll' && displayGroups.length && !hasMoreBrowse && !hasMoreVisible && !hasMore && !loading"
|
||||||
|
class="py-3 text-center text-xs text-dimmed"
|
||||||
|
>
|
||||||
|
No hay más resultados
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="settings.paginationType === 'numbered' && totalPages > 1 && !loading"
|
||||||
|
class="px-4 py-3 border-t border-default flex justify-center shrink-0"
|
||||||
|
>
|
||||||
|
<UPagination
|
||||||
|
:page="activePage"
|
||||||
|
:total="displayTotal"
|
||||||
|
:items-per-page="settings.pageSize"
|
||||||
|
size="sm"
|
||||||
|
@update:page="goToPage"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</UDashboardPanel>
|
||||||
|
|
||||||
|
<!-- Panel de detalle (escritorio) -->
|
||||||
|
<PublicationDetail
|
||||||
|
v-if="selectedDocId && !isMobile"
|
||||||
|
:document="selectedDocument"
|
||||||
|
:document-loading="documentLoading"
|
||||||
|
:paragraphs="selectedParagraphs"
|
||||||
|
:paragraphs-loading="paragraphsLoading"
|
||||||
|
:collection="favoritesCollection"
|
||||||
|
:query="debouncedQuery"
|
||||||
|
:selected-hit="selectedHit"
|
||||||
|
:selected-matching-hits="selectedMatchingHits"
|
||||||
|
:accent-color="accentColor"
|
||||||
|
@close="isPanelOpen = false"
|
||||||
|
/>
|
||||||
|
<div v-else-if="!isMobile" class="hidden lg:flex flex-1 items-center justify-center">
|
||||||
|
<div class="flex flex-col items-center gap-2 text-dimmed">
|
||||||
|
<UIcon name="i-lucide-search" class="size-16" />
|
||||||
|
<p class="text-sm">{{ emptyDetailText }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Panel de detalle (móvil) -->
|
||||||
|
<ClientOnly>
|
||||||
|
<USlideover v-if="isMobile" v-model:open="isPanelOpen">
|
||||||
|
<template #content>
|
||||||
|
<PublicationDetail
|
||||||
|
v-if="selectedDocId"
|
||||||
|
:document="selectedDocument"
|
||||||
|
:document-loading="documentLoading"
|
||||||
|
:paragraphs="selectedParagraphs"
|
||||||
|
:paragraphs-loading="paragraphsLoading"
|
||||||
|
:collection="favoritesCollection"
|
||||||
|
:query="debouncedQuery"
|
||||||
|
:selected-hit="selectedHit"
|
||||||
|
:selected-matching-hits="selectedMatchingHits"
|
||||||
|
:accent-color="accentColor"
|
||||||
|
@close="isPanelOpen = false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</USlideover>
|
||||||
|
</ClientOnly>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.snippet-html :deep(p) {
|
||||||
|
display: inline;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.snippet-html :deep(br) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
const STORAGE_KEY = 'entrelineas_dev_unlocked'
|
||||||
|
|
||||||
|
export function useDevMode() {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const devKey = config.public.entrelineasDevKey as string
|
||||||
|
|
||||||
|
const unlocked = ref(false)
|
||||||
|
|
||||||
|
if (import.meta.client) {
|
||||||
|
unlocked.value = localStorage.getItem(STORAGE_KEY) === 'true'
|
||||||
|
}
|
||||||
|
|
||||||
|
function unlock(key: string): boolean {
|
||||||
|
if (!devKey) return false
|
||||||
|
if (key === devKey) {
|
||||||
|
unlocked.value = true
|
||||||
|
if (import.meta.client) {
|
||||||
|
localStorage.setItem(STORAGE_KEY, 'true')
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
function lock() {
|
||||||
|
unlocked.value = false
|
||||||
|
if (import.meta.client) {
|
||||||
|
localStorage.removeItem(STORAGE_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { unlocked, unlock, lock }
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
let loaded = false
|
||||||
|
let loading = false
|
||||||
|
let loadPromise: Promise<void> | null = null
|
||||||
|
|
||||||
|
function loadScript(siteKey: string): Promise<void> {
|
||||||
|
if (loaded) return Promise.resolve()
|
||||||
|
if (loading && loadPromise) return loadPromise
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
loadPromise = new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement('script')
|
||||||
|
script.src = `https://www.google.com/recaptcha/api.js?render=${siteKey}`
|
||||||
|
script.async = true
|
||||||
|
script.defer = true
|
||||||
|
script.onload = () => {
|
||||||
|
loaded = true
|
||||||
|
loading = false
|
||||||
|
resolve()
|
||||||
|
}
|
||||||
|
script.onerror = () => {
|
||||||
|
loading = false
|
||||||
|
reject(new Error('Failed to load reCAPTCHA'))
|
||||||
|
}
|
||||||
|
document.head.appendChild(script)
|
||||||
|
})
|
||||||
|
|
||||||
|
return loadPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRecaptcha(siteKey: string) {
|
||||||
|
async function executeRecaptcha(action: string = 'submit'): Promise<string> {
|
||||||
|
await loadScript(siteKey)
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
window.grecaptcha.ready(() => {
|
||||||
|
window.grecaptcha
|
||||||
|
.execute(siteKey, { action })
|
||||||
|
.then((token: string) => resolve(token))
|
||||||
|
.catch(reject)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { executeRecaptcha }
|
||||||
|
}
|
||||||
|
|
@ -4,9 +4,13 @@ import { storeToRefs } from 'pinia'
|
||||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||||
import { useFavoritesStore } from '~/stores/favorites'
|
import { useFavoritesStore } from '~/stores/favorites'
|
||||||
import { useHistoryStore } from '~/stores/history'
|
import { useHistoryStore } from '~/stores/history'
|
||||||
|
import { chatPalette } from '#build/ui'
|
||||||
|
|
||||||
|
const unlocked = ref(useDevMode())
|
||||||
|
|
||||||
const { locale, locales, setLocale } = useI18n()
|
const { locale, locales, setLocale } = useI18n()
|
||||||
|
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
const { $i18n } = useNuxtApp();
|
const { $i18n } = useNuxtApp();
|
||||||
const t = $i18n.t;
|
const t = $i18n.t;
|
||||||
|
|
||||||
|
|
@ -20,26 +24,30 @@ const { total: histTotal } = storeToRefs(history)
|
||||||
|
|
||||||
|
|
||||||
const links = computed(() => {
|
const links = computed(() => {
|
||||||
return [
|
const links = [
|
||||||
{
|
{
|
||||||
|
id: 'bible-studies',
|
||||||
label: t('nav.bible_studies'),
|
label: t('nav.bible_studies'),
|
||||||
icon: 'ph:books',
|
icon: 'ph-books',
|
||||||
to: '/estudios-biblicos',
|
to: '/estudios-biblicos',
|
||||||
onSelect: () => { open.value = false }
|
onSelect: () => { open.value = false },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'conferences',
|
||||||
label: t('nav.conferences'),
|
label: t('nav.conferences'),
|
||||||
icon: 'ph:books',
|
icon: 'ph-books',
|
||||||
to: '/conferencias',
|
to: '/conferencias',
|
||||||
onSelect: () => { open.value = false }
|
onSelect: () => { open.value = false }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'betweenthelines',
|
||||||
label: t('nav.between_the_lines'),
|
label: t('nav.between_the_lines'),
|
||||||
icon: 'ph:list-magnifying-glass',
|
icon: 'ph-list-magnifying-glass',
|
||||||
to: '/entrelineas',
|
to: '/entrelineas',
|
||||||
onSelect: () => { open.value = false }
|
onSelect: () => { open.value = false }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'favorites',
|
||||||
label: t('nav.my_list'),
|
label: t('nav.my_list'),
|
||||||
icon: 'i-lucide-bookmark',
|
icon: 'i-lucide-bookmark',
|
||||||
to: '/favoritos',
|
to: '/favoritos',
|
||||||
|
|
@ -47,6 +55,7 @@ const links = computed(() => {
|
||||||
onSelect: () => { open.value = false }
|
onSelect: () => { open.value = false }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'history',
|
||||||
label: t('nav.history'),
|
label: t('nav.history'),
|
||||||
icon: 'i-lucide-history',
|
icon: 'i-lucide-history',
|
||||||
to: '/historial',
|
to: '/historial',
|
||||||
|
|
@ -54,50 +63,88 @@ const links = computed(() => {
|
||||||
onSelect: () => { open.value = false }
|
onSelect: () => { open.value = false }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: 'changelog',
|
||||||
|
label: t('nav.changelog'),
|
||||||
|
icon: 'i-lucide-megaphone',
|
||||||
|
to: '/changelog',
|
||||||
|
onSelect: () => { open.value = false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'settings',
|
||||||
label: t('nav.settings'),
|
label: t('nav.settings'),
|
||||||
icon: 'i-lucide-settings',
|
icon: 'i-lucide-settings',
|
||||||
to: '/configuracion',
|
to: '/configuracion',
|
||||||
onSelect: () => { open.value = false }
|
onSelect: () => { open.value = false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'feedback',
|
||||||
|
label: t('feedback.title'),
|
||||||
|
icon: 'i-lucide-bug',
|
||||||
|
to: '/feedback',
|
||||||
|
onSelect: () => { open.value = false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wizard',
|
||||||
|
class: 'mt-4 border-t-2 border-gray-300 pt-4',
|
||||||
|
label: t('nav.tour'),
|
||||||
|
icon: 'ph-student',
|
||||||
|
onSelect: () => { doTour() },
|
||||||
|
chip: {
|
||||||
|
color: 'error'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
] satisfies NavigationMenuItem[]
|
|
||||||
})
|
|
||||||
|
|
||||||
|
] satisfies NavigationMenuItem[]
|
||||||
|
|
||||||
|
function doTour(){
|
||||||
|
nuxtApp.callHook('tour',0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const homeLink = {
|
||||||
|
id: 'home',
|
||||||
|
label: t('nav.home'),
|
||||||
|
icon: 'ph-house',
|
||||||
|
to: '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
return [homeLink, ...links]
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UDashboardGroup unit="rem">
|
<UDashboardGroup unit="rem">
|
||||||
<UDashboardSidebar
|
<UDashboardSidebar id="default" v-model:open="open" collapsible resizable
|
||||||
id="default"
|
class="bg-elevated/25 bg-gradient-to-tr from-blue-100 to-white" :ui="{ footer: 'lg:border-t lg:border-default' }">
|
||||||
v-model:open="open"
|
|
||||||
collapsible
|
|
||||||
resizable
|
|
||||||
class="bg-elevated/25 bg-gradient-to-tr from-blue-100 to-white"
|
|
||||||
:ui="{ footer: 'lg:border-t lg:border-default' }"
|
|
||||||
>
|
|
||||||
<template #header="{ collapsed }">
|
<template #header="{ collapsed }">
|
||||||
<div v-if="!collapsed" class="mt-2 flex justify-center">
|
<div v-if="!collapsed" class="mt-2 flex justify-center">
|
||||||
<img src="/logo.svg" class="w-full" alt="Buscador - La Gran Carpa Catedral" />
|
<img src="/logo.svg" class="w-full" alt="Buscador - La Gran Carpa Catedral" />
|
||||||
</div>
|
</div>
|
||||||
|
<img v-if="collapsed" src="/logo_round.svg" class="w-full" alt="Buscador - La Gran Carpa Catedral" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="{ collapsed }">
|
<template #default="{ collapsed }">
|
||||||
<UNavigationMenu
|
<UNavigationMenu :collapsed="collapsed" :items="links" orientation="vertical" tooltip popover color="neutral"
|
||||||
:collapsed="collapsed"
|
variant="link">
|
||||||
:items="links"
|
|
||||||
orientation="vertical"
|
<template #item="{ item }">
|
||||||
tooltip
|
<div :id="item.id"
|
||||||
popover
|
class="group relative w-full flex items-center gap-1.5 font-medium text-sm before:absolute before:z-[-1] before:rounded-md focus:outline-none focus-visible:outline-none dark:focus-visible:outline-none focus-visible:before:ring-inset focus-visible:before:ring-2 focus-visible:before:ring-inverted flex-row">
|
||||||
color="neutral"
|
<UChip :color="item.chip?.color" :show="item.chip?.color ? true : false" inset>
|
||||||
variant="link"
|
<UIcon :name="item.icon" class="text-2xl" />
|
||||||
/>
|
</UChip>
|
||||||
|
<div :class="!collapsed ? 'flex justify-between w-full' : 'hidden'">{{ item.label }}
|
||||||
|
<UBadge v-if="item.badge" variant="outline" color="neutral" size="sm" :label="item.badge" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UNavigationMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #footer="{ collapsed }">
|
<template #footer="{ collapsed }">
|
||||||
<ULocaleSelect
|
<div id="localeSelector">
|
||||||
:model-value="$i18n.locale.value"
|
<ULocaleSelect :model-value="$i18n.locale.value" :locales="Object.values(locales)"
|
||||||
:locales="Object.values(locales)"
|
@update:model-value="setLocale($event)" />
|
||||||
@update:model-value="setLocale($event)"
|
</div>
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</UDashboardSidebar>
|
</UDashboardSidebar>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
const t = $i18n.t
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardPanel id="changelog-panel" grow>
|
||||||
|
<UDashboardNavbar :title="t('nav.changelog')">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<div class="overflow-y-auto flex-1">
|
||||||
|
<div class="max-w-2xl mx-auto p-6 space-y-10">
|
||||||
|
<div v-for="release in releases" :key="release.version" class="relative pl-6 border-l-2 border-default">
|
||||||
|
<!-- Dot en la línea de tiempo -->
|
||||||
|
<div class="absolute -left-[5px] top-1 w-2 h-2 rounded-full bg-primary" />
|
||||||
|
|
||||||
|
<!-- Cabecera del release -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
|
<UBadge color="primary" variant="soft" size="sm">
|
||||||
|
v{{ release.version }}
|
||||||
|
</UBadge>
|
||||||
|
<span class="text-xs text-muted">{{ release.date }}</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-base font-semibold text-highlighted mt-1">
|
||||||
|
{{ release.title }}
|
||||||
|
</h2>
|
||||||
|
<p v-if="release.description" class="text-sm text-muted mt-0.5">
|
||||||
|
{{ release.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Lista de cambios -->
|
||||||
|
<ul class="space-y-2">
|
||||||
|
<li
|
||||||
|
v-for="(change, i) in release.changes"
|
||||||
|
:key="i"
|
||||||
|
class="flex items-start gap-2 text-sm"
|
||||||
|
>
|
||||||
|
<UBadge
|
||||||
|
:color="typeConfig[change.type].color as any"
|
||||||
|
variant="subtle"
|
||||||
|
size="xs"
|
||||||
|
class="mt-0.5 shrink-0"
|
||||||
|
>
|
||||||
|
{{ typeConfig[change.type].label }}
|
||||||
|
</UBadge>
|
||||||
|
<span class="text-default">{{ change.text }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
|
|
@ -1,787 +1,12 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
||||||
import { breakpointsTailwind, useDebounce } from '@vueuse/core'
|
|
||||||
import EstudiosTypensenseDetail from '~/components/estudiosTypensense/EstudiosTypensenseDetail.vue'
|
|
||||||
import { useSettingsStore } from '~/stores/settings'
|
|
||||||
|
|
||||||
const PARAGRAPHS_COLLECTION = 'paragraphs'
|
|
||||||
const DOCUMENTS_COLLECTION = 'documents'
|
|
||||||
const QUERY_BY = 'text'
|
|
||||||
const FAVORITES_COLLECTION = 'conferences-ts'
|
|
||||||
|
|
||||||
const { $i18n } = useNuxtApp()
|
|
||||||
const t = $i18n.t
|
|
||||||
const { locale } = useI18n()
|
|
||||||
|
|
||||||
const filterBy = computed(() => `locale:=${locale.value} && type:=conferences`)
|
|
||||||
const REQUEST_TIMEOUT_MS = 15000
|
|
||||||
|
|
||||||
const settings = useSettingsStore()
|
|
||||||
|
|
||||||
// ── Restaurar estado desde URL antes de crear los refs ─────────────────────
|
|
||||||
const { query: q0, page: p0, scroll: s0, selectedId: sid0 } = useSearchUrlState()
|
|
||||||
|
|
||||||
const query = ref(q0)
|
|
||||||
const debouncedQuery = useDebounce(query, 150)
|
|
||||||
const loading = ref(false)
|
|
||||||
const loadingMore = ref(false)
|
|
||||||
const errorMsg = ref<string | null>(null)
|
|
||||||
|
|
||||||
// ---- Types ----------------------------------------------------------------
|
|
||||||
|
|
||||||
interface ParagraphDoc {
|
|
||||||
id?: string
|
|
||||||
document_id: string
|
|
||||||
text: string
|
|
||||||
number: number
|
|
||||||
locale: string
|
|
||||||
type: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DocMeta {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
date?: string
|
|
||||||
timestamp?: number
|
|
||||||
place?: string
|
|
||||||
city?: string
|
|
||||||
state?: string
|
|
||||||
country?: string
|
|
||||||
type?: string
|
|
||||||
slug?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocumentDoc extends DocMeta {
|
|
||||||
code: string
|
|
||||||
locale: string
|
|
||||||
files?: {
|
|
||||||
youtube?: string
|
|
||||||
video?: string
|
|
||||||
audio?: string
|
|
||||||
booklet?: string
|
|
||||||
simple?: string
|
|
||||||
}
|
|
||||||
body?: string
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TypesenseHighlight {
|
|
||||||
field?: string
|
|
||||||
snippet?: string
|
|
||||||
value?: string
|
|
||||||
matched_tokens?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TypesenseParagraphHit {
|
|
||||||
document: ParagraphDoc
|
|
||||||
highlights?: TypesenseHighlight[]
|
|
||||||
highlight?: Record<string, { snippet?: string, value?: string }>
|
|
||||||
text_match?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TypesenseGroupedHit {
|
|
||||||
group_key: string[]
|
|
||||||
hits: TypesenseParagraphHit[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TypesenseSearchResponse {
|
|
||||||
found: number
|
|
||||||
grouped_hits?: TypesenseGroupedHit[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchGroup {
|
|
||||||
docId: string
|
|
||||||
firstHit: TypesenseParagraphHit
|
|
||||||
allHits: TypesenseParagraphHit[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BrowseItem {
|
|
||||||
docId: string
|
|
||||||
meta: DocMeta
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DisplayGroup {
|
|
||||||
docId: string
|
|
||||||
meta: DocMeta | undefined
|
|
||||||
firstHit: TypesenseParagraphHit | null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- State ----------------------------------------------------------------
|
|
||||||
|
|
||||||
// Modo búsqueda (con query): grupos devueltos por Typesense
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
// Progressive display (sólo en scroll infinito)
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
// Modo exploración (sin query): documentos ordenados por fecha
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
// Grupo unificado para el template
|
|
||||||
const displayGroups = computed((): DisplayGroup[] => {
|
|
||||||
if (!debouncedQuery.value.trim()) {
|
|
||||||
return browseItems.value.map(item => ({
|
|
||||||
docId: item.docId,
|
|
||||||
meta: item.meta,
|
|
||||||
firstHit: null
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return visibleGroups.value.map(g => ({
|
|
||||||
docId: g.docId,
|
|
||||||
meta: docCache.value[g.docId],
|
|
||||||
firstHit: g.firstHit
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Paginación activa (compartida entre browse y search)
|
|
||||||
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))
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cache de metadatos por document_id (sólo modo búsqueda)
|
|
||||||
const docCache = ref<Record<string, DocMeta>>({})
|
|
||||||
|
|
||||||
const { documentsApi } = useTypesenseApi()
|
|
||||||
|
|
||||||
// ---- Batch fetch de metadatos de documentos -------------------------------
|
|
||||||
|
|
||||||
async function fetchDocumentMeta(docIds: string[]) {
|
|
||||||
const unique = docIds.filter(id => id && !(id in docCache.value))
|
|
||||||
console.log('Fetching metadata for documents', unique)
|
|
||||||
try {
|
|
||||||
const res = await documentsApi.multiSearch({
|
|
||||||
multiSearchParameters: {},
|
|
||||||
multiSearchSearchesParameter: {
|
|
||||||
searches: [{
|
|
||||||
collection: DOCUMENTS_COLLECTION,
|
|
||||||
q: '*',
|
|
||||||
queryBy: 'title',
|
|
||||||
filterBy: `id:=[${unique.join(',')}]`,
|
|
||||||
includeFields: 'id,title,date,timestamp,place,city,state,country,type,slug',
|
|
||||||
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 multi = await documentsApi.multiSearch({
|
|
||||||
multiSearchParameters: {},
|
|
||||||
multiSearchSearchesParameter: {
|
|
||||||
searches: [{
|
|
||||||
collection: PARAGRAPHS_COLLECTION,
|
|
||||||
q: q || '*',
|
|
||||||
queryBy: QUERY_BY,
|
|
||||||
filterBy: filterBy.value,
|
|
||||||
perPage: settings.pageSize,
|
|
||||||
page: typePage,
|
|
||||||
highlightFullFields: QUERY_BY,
|
|
||||||
highlightFields: QUERY_BY,
|
|
||||||
highlightStartTag: '<mark class="search-match">',
|
|
||||||
highlightEndTag: '</mark>',
|
|
||||||
highlightAffixNumTokens: 30,
|
|
||||||
groupBy: 'document_id'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (seq !== searchSeq) return
|
|
||||||
|
|
||||||
const res = (multi?.results?.[0] ?? {}) as TypesenseSearchResponse
|
|
||||||
const rawGroups = res?.groupedHits ?? []
|
|
||||||
|
|
||||||
console.log('Raw groups', rawGroups)
|
|
||||||
const newGroups: SearchGroup[] = rawGroups.map(g => ({
|
|
||||||
docId: g.groupKey[0]!,
|
|
||||||
firstHit: g.hits[0]!,
|
|
||||||
allHits: g.hits
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (!append) docCache.value = {}
|
|
||||||
const newDocIds = newGroups.map(g => g.docId).filter(Boolean)
|
|
||||||
await fetchDocumentMeta(newDocIds)
|
|
||||||
|
|
||||||
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: DOCUMENTS_COLLECTION,
|
|
||||||
q: '*',
|
|
||||||
queryBy: 'title',
|
|
||||||
filterBy: filterBy.value,
|
|
||||||
sortBy: 'timestamp:desc',
|
|
||||||
perPage: settings.pageSize,
|
|
||||||
page: typePage,
|
|
||||||
includeFields: 'id,title,date,timestamp,place,city,state,country,type,slug'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (seq !== searchSeq) return
|
|
||||||
|
|
||||||
const result = (multi?.results?.[0] as { found?: number, hits?: Array<{ document: DocMeta }> } | undefined)
|
|
||||||
const newItems = (result?.hits ?? []).map(h => ({
|
|
||||||
docId: h.document.id!,
|
|
||||||
meta: h.document
|
|
||||||
}))
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const listContainer = ref<HTMLElement | null>(null)
|
|
||||||
|
|
||||||
function onListScroll() {
|
|
||||||
if (settings.paginationType !== 'infinite_scroll') return
|
|
||||||
const el = listContainer.value
|
|
||||||
if (!el) return
|
|
||||||
if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
|
|
||||||
if (!debouncedQuery.value.trim()) {
|
|
||||||
if (hasMoreBrowse.value && !loadingMore.value && !loading.value) {
|
|
||||||
runBrowse(browsePage.value, true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (hasMoreVisible.value) {
|
|
||||||
visibleGroupCount.value += 10
|
|
||||||
} else if (hasMore.value && !loadingMore.value && !loading.value) {
|
|
||||||
loadMore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function retry() {
|
|
||||||
if (!query.value.trim()) {
|
|
||||||
runBrowse(activePage.value, false)
|
|
||||||
} else {
|
|
||||||
runSearch(query.value, activePage.value, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeUnmount(() => { if (timeoutId) clearTimeout(timeoutId) })
|
|
||||||
|
|
||||||
watch(debouncedQuery, (q) => {
|
|
||||||
activePage.value = 1
|
|
||||||
if (!q.trim()) {
|
|
||||||
groupedHits.value = []
|
|
||||||
total.value = 0
|
|
||||||
currentPage.value = 1
|
|
||||||
visibleGroupCount.value = 10
|
|
||||||
browseItems.value = []
|
|
||||||
browseTotal.value = 0
|
|
||||||
browsePage.value = 1
|
|
||||||
runBrowse(1, false)
|
|
||||||
} else {
|
|
||||||
browseItems.value = []
|
|
||||||
browseTotal.value = 0
|
|
||||||
browsePage.value = 1
|
|
||||||
groupedHits.value = []
|
|
||||||
total.value = 0
|
|
||||||
currentPage.value = 1
|
|
||||||
visibleGroupCount.value = 10
|
|
||||||
runSearch(q, 1, false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// ---- Selección y carga del detalle ----------------------------------------
|
|
||||||
|
|
||||||
const selectedDocId = ref<string | null>(null)
|
|
||||||
const selectedDocument = ref<DocumentDoc | null>(null)
|
|
||||||
const documentLoading = ref(false)
|
|
||||||
const selectedParagraphs = ref<TypesenseParagraphHit[]>([])
|
|
||||||
const paragraphsLoading = ref(false)
|
|
||||||
|
|
||||||
const selectedHit = ref<TypesenseParagraphHit | null>(null)
|
|
||||||
const selectedMatchingHits = ref<TypesenseParagraphHit[]>([])
|
|
||||||
|
|
||||||
async function fetchFullDocument(docId: string) {
|
|
||||||
documentLoading.value = true
|
|
||||||
selectedDocument.value = null
|
|
||||||
try {
|
|
||||||
const res = await documentsApi.multiSearch({
|
|
||||||
multiSearchParameters: {},
|
|
||||||
multiSearchSearchesParameter: {
|
|
||||||
searches: [{
|
|
||||||
collection: DOCUMENTS_COLLECTION,
|
|
||||||
q: '*',
|
|
||||||
queryBy: 'title',
|
|
||||||
filterBy: `id:=${docId}`,
|
|
||||||
perPage: 1,
|
|
||||||
page: 1
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const docHits = (res?.results?.[0] as { hits?: Array<{ document: DocumentDoc }> })?.hits ?? []
|
|
||||||
selectedDocument.value = docHits[0]?.document ?? null
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching document', err)
|
|
||||||
selectedDocument.value = null
|
|
||||||
} finally {
|
|
||||||
documentLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchDocumentParagraphs(docId: string) {
|
|
||||||
paragraphsLoading.value = true
|
|
||||||
selectedParagraphs.value = []
|
|
||||||
const PER_PAGE = 250
|
|
||||||
let page = 1
|
|
||||||
let totalParagraphs = 0
|
|
||||||
const all: Array<{ document: ParagraphDoc }> = []
|
|
||||||
try {
|
|
||||||
do {
|
|
||||||
const res = await documentsApi.multiSearch({
|
|
||||||
multiSearchParameters: {},
|
|
||||||
multiSearchSearchesParameter: {
|
|
||||||
searches: [{
|
|
||||||
collection: PARAGRAPHS_COLLECTION,
|
|
||||||
q: '*',
|
|
||||||
queryBy: '',
|
|
||||||
filterBy: `document_id:=${docId} && ${filterBy.value}`,
|
|
||||||
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) totalParagraphs = result.found ?? 0
|
|
||||||
all.push(...(result.hits ?? []))
|
|
||||||
page++
|
|
||||||
} while (all.length < totalParagraphs)
|
|
||||||
selectedParagraphs.value = all.map(h => ({ document: h.document }))
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching paragraphs', err)
|
|
||||||
selectedParagraphs.value = []
|
|
||||||
} finally {
|
|
||||||
paragraphsLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectGroup(group: DisplayGroup) {
|
|
||||||
selectedDocId.value = group.docId
|
|
||||||
selectedHit.value = group.firstHit
|
|
||||||
const searchGroup = groupedHits.value.find(g => g.docId === group.docId)
|
|
||||||
selectedMatchingHits.value = searchGroup?.allHits ?? []
|
|
||||||
fetchFullDocument(group.docId)
|
|
||||||
fetchDocumentParagraphs(group.docId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPanelOpen = computed({
|
|
||||||
get() { return !!selectedDocId.value },
|
|
||||||
set(v: boolean) {
|
|
||||||
if (!v) {
|
|
||||||
selectedDocId.value = null
|
|
||||||
selectedDocument.value = null
|
|
||||||
selectedParagraphs.value = []
|
|
||||||
selectedHit.value = null
|
|
||||||
selectedMatchingHits.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(groupedHits, () => {
|
|
||||||
if (!selectedDocId.value || !debouncedQuery.value.trim()) return
|
|
||||||
const still = groupedHits.value.find(g => g.docId === selectedDocId.value)
|
|
||||||
if (!still) {
|
|
||||||
selectedDocId.value = null
|
|
||||||
selectedDocument.value = null
|
|
||||||
selectedParagraphs.value = []
|
|
||||||
selectedHit.value = null
|
|
||||||
selectedMatchingHits.value = []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// ID del documento seleccionado para sincronizar con la URL
|
|
||||||
const selectedId = computed(() => selectedDocId.value)
|
|
||||||
|
|
||||||
// ── Sincronización con URL ─────────────────────────────────────────────────
|
|
||||||
useSearchUrlSync({ query, page: activePage, selectedId, scrollEl: listContainer })
|
|
||||||
|
|
||||||
// ── Ciclo de vida ──────────────────────────────────────────────────────────
|
|
||||||
onMounted(async () => {
|
|
||||||
// Carga inicial: browse si no hay query, search si hay query
|
|
||||||
if (q0.trim()) {
|
|
||||||
await runSearch(q0, p0, false)
|
|
||||||
} else {
|
|
||||||
await runBrowse(p0, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restaurar scroll
|
|
||||||
restoreScrollPosition(listContainer.value, s0)
|
|
||||||
|
|
||||||
// Restaurar documento seleccionado
|
|
||||||
if (sid0) {
|
|
||||||
const group = displayGroups.value.find(g => g.docId === sid0)
|
|
||||||
if (group) {
|
|
||||||
selectGroup(group)
|
|
||||||
} else {
|
|
||||||
selectedDocId.value = sid0
|
|
||||||
fetchFullDocument(sid0)
|
|
||||||
fetchDocumentParagraphs(sid0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
|
||||||
const isMobile = breakpoints.smaller('lg')
|
|
||||||
|
|
||||||
useDetailHistory(isPanelOpen, isMobile)
|
|
||||||
|
|
||||||
// ---- Helpers de presentación ----------------------------------------------
|
|
||||||
|
|
||||||
function highlightedFor(hit: TypesenseParagraphHit, field: string): string | null {
|
|
||||||
const fromArr = hit.highlights?.find(h => h.field === field)
|
|
||||||
if (fromArr?.snippet) return fromArr.snippet
|
|
||||||
if (fromArr?.value) return fromArr.value
|
|
||||||
const fromObj = hit.highlight?.[field]
|
|
||||||
if (fromObj?.snippet) return fromObj.snippet
|
|
||||||
if (fromObj?.value) return fromObj.value
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function metaDate(meta: DocMeta | undefined): string {
|
|
||||||
if (!meta) return ''
|
|
||||||
const ts = meta.timestamp || (meta.date ? Math.floor(new Date(meta.date).getTime() / 1000) : null)
|
|
||||||
if (!ts) return meta.date || ''
|
|
||||||
return formatDate(ts)
|
|
||||||
}
|
|
||||||
|
|
||||||
function metaLocation(meta: DocMeta | undefined): string {
|
|
||||||
if (!meta) return ''
|
|
||||||
return formatLocation({
|
|
||||||
id: meta.id,
|
|
||||||
date: meta.timestamp ?? 0,
|
|
||||||
slug: meta.slug ?? '',
|
|
||||||
type: meta.type ?? '',
|
|
||||||
place: meta.place ?? '',
|
|
||||||
city: meta.city ?? '',
|
|
||||||
state: meta.state ?? '',
|
|
||||||
country: meta.country ?? '',
|
|
||||||
thumbnail: ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UDashboardPanel
|
<SearchPanel
|
||||||
id="conferencias-ts-list"
|
paragraphs-collection="conferences_paragraphs"
|
||||||
:default-size="32"
|
main-collection="conferences"
|
||||||
:min-size="24"
|
group-by-field="conferences_id"
|
||||||
:max-size="45"
|
favorites-collection="conferences-ts"
|
||||||
resizable
|
panel-id="conferencias-ts-list"
|
||||||
>
|
nav-title-key="nav.conferences_ts"
|
||||||
<UDashboardNavbar :title="t('nav.conferences_ts')">
|
accent-color="blue"
|
||||||
<template #leading>
|
:empty-detail-text="$t('ui.empty_conferences')"
|
||||||
<UDashboardSidebarCollapse />
|
|
||||||
</template>
|
|
||||||
<template #trailing>
|
|
||||||
<UBadge :label="displayTotal" variant="subtle" />
|
|
||||||
</template>
|
|
||||||
</UDashboardNavbar>
|
|
||||||
|
|
||||||
<div class="px-4 sm:px-6 py-3 border-b border-default">
|
|
||||||
<UInput
|
|
||||||
v-model="query"
|
|
||||||
icon="i-lucide-search"
|
|
||||||
:placeholder="t('search.placeholder')"
|
|
||||||
:loading="loading"
|
|
||||||
size="md"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UAlert
|
|
||||||
v-if="errorMsg"
|
|
||||||
:title="errorMsg"
|
|
||||||
color="error"
|
|
||||||
variant="subtle"
|
|
||||||
icon="i-lucide-triangle-alert"
|
|
||||||
class="mx-4 my-2"
|
|
||||||
:actions="[{ label: 'Reintentar', color: 'neutral', variant: 'outline', onClick: retry }]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div ref="listContainer" class="overflow-y-auto divide-y divide-default flex-1" @scroll="onListScroll">
|
|
||||||
<div
|
|
||||||
v-if="loading && !displayGroups.length"
|
|
||||||
class="flex items-center justify-center gap-2 py-16 text-sm text-muted"
|
|
||||||
>
|
|
||||||
<UIcon name="i-lucide-loader-circle" class="size-4 animate-spin" />
|
|
||||||
Buscando...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else-if="!displayGroups.length"
|
|
||||||
class="flex flex-col items-center justify-center gap-2 py-16 text-dimmed text-sm"
|
|
||||||
>
|
|
||||||
<UIcon name="i-lucide-inbox" class="size-10" />
|
|
||||||
<p>{{ query ? `Sin coincidencias para "${query}"` : 'Sin resultados' }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-for="group in displayGroups"
|
|
||||||
:key="group.docId"
|
|
||||||
class="p-4 sm:px-6 text-sm cursor-pointer border-b-2 transition-colors"
|
|
||||||
:class="[
|
|
||||||
selectedDocId === group.docId
|
|
||||||
? 'border-carpablue bg-carpablue/10'
|
|
||||||
: 'border-gray-200 hover:border-carpablue hover:bg-carpablue/5'
|
|
||||||
]"
|
|
||||||
@click="selectGroup(group)"
|
|
||||||
>
|
|
||||||
<div class="mb-1">
|
|
||||||
<p class="text-sm font-semibold line-clamp-2 text-highlighted">
|
|
||||||
{{ group.meta?.title || group.docId }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 text-xs mb-2 text-muted">
|
|
||||||
<span
|
|
||||||
v-if="metaDate(group.meta)"
|
|
||||||
class="flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<UIcon name="ph:calendar" class="size-4 text-carpablue" />
|
|
||||||
{{ metaDate(group.meta) }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="metaLocation(group.meta)"
|
|
||||||
class="flex items-center gap-1 truncate"
|
|
||||||
>
|
|
||||||
<UIcon name="ph:map-pin" class="size-4 text-carpablue shrink-0" />
|
|
||||||
<span class="truncate">{{ metaLocation(group.meta) }}</span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Snippet con highlight: solo en modo búsqueda -->
|
|
||||||
<div
|
|
||||||
v-if="group.firstHit"
|
|
||||||
class="snippet-html text-sm text-dimmed"
|
|
||||||
v-html="highlightedFor(group.firstHit, 'text') || group.firstHit.document.text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Infinite scroll: cargando más -->
|
|
||||||
<div
|
|
||||||
v-if="settings.paginationType === 'infinite_scroll' && loadingMore"
|
|
||||||
class="flex items-center justify-center gap-2 py-4 text-sm text-muted"
|
|
||||||
>
|
|
||||||
<UIcon name="i-lucide-loader-circle" class="size-4 animate-spin" />
|
|
||||||
Cargando más...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else-if="settings.paginationType === 'infinite_scroll' && displayGroups.length && !hasMoreBrowse && !hasMoreVisible && !hasMore && !loading"
|
|
||||||
class="py-3 text-center text-xs text-dimmed"
|
|
||||||
>
|
|
||||||
No hay más resultados
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Paginación numerada -->
|
|
||||||
<div
|
|
||||||
v-if="settings.paginationType === 'numbered' && totalPages > 1 && !loading"
|
|
||||||
class="px-4 py-3 border-t border-default flex justify-center shrink-0"
|
|
||||||
>
|
|
||||||
<UPagination
|
|
||||||
:page="activePage"
|
|
||||||
:total="displayTotal"
|
|
||||||
:items-per-page="settings.pageSize"
|
|
||||||
size="sm"
|
|
||||||
@update:page="goToPage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</UDashboardPanel>
|
|
||||||
|
|
||||||
<!-- Panel de detalle (escritorio) -->
|
|
||||||
<EstudiosTypensenseDetail
|
|
||||||
v-if="selectedDocId && !isMobile"
|
|
||||||
:document="selectedDocument"
|
|
||||||
:document-loading="documentLoading"
|
|
||||||
:paragraphs="selectedParagraphs"
|
|
||||||
:paragraphs-loading="paragraphsLoading"
|
|
||||||
:collection="FAVORITES_COLLECTION"
|
|
||||||
:query="debouncedQuery"
|
|
||||||
:selected-hit="selectedHit"
|
|
||||||
:selected-matching-hits="selectedMatchingHits"
|
|
||||||
@close="isPanelOpen = false"
|
|
||||||
/>
|
/>
|
||||||
<div v-else-if="!isMobile" class="hidden lg:flex flex-1 items-center justify-center">
|
|
||||||
<div class="flex flex-col items-center gap-2 text-dimmed">
|
|
||||||
<UIcon name="i-lucide-search" class="size-16" />
|
|
||||||
<p class="text-sm">Selecciona una conferencia para ver el detalle</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Panel de detalle (móvil) -->
|
|
||||||
<ClientOnly>
|
|
||||||
<USlideover v-if="isMobile" v-model:open="isPanelOpen">
|
|
||||||
<template #content>
|
|
||||||
<EstudiosTypensenseDetail
|
|
||||||
v-if="selectedDocId"
|
|
||||||
:document="selectedDocument"
|
|
||||||
:document-loading="documentLoading"
|
|
||||||
:paragraphs="selectedParagraphs"
|
|
||||||
:paragraphs-loading="paragraphsLoading"
|
|
||||||
:collection="FAVORITES_COLLECTION"
|
|
||||||
:query="debouncedQuery"
|
|
||||||
:selected-hit="selectedHit"
|
|
||||||
:selected-matching-hits="selectedMatchingHits"
|
|
||||||
@close="isPanelOpen = false"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</USlideover>
|
|
||||||
</ClientOnly>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.snippet-html :deep(p) {
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.snippet-html :deep(br) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,19 @@ const paginationItems = [
|
||||||
description: t('settings.numbered_desc')
|
description: t('settings.numbered_desc')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const { unlocked, unlock, lock } = useDevMode()
|
||||||
|
const devKeyInput = ref('')
|
||||||
|
const devKeyError = ref('')
|
||||||
|
|
||||||
|
function tryUnlock() {
|
||||||
|
devKeyError.value = ''
|
||||||
|
if (unlock(devKeyInput.value)) {
|
||||||
|
devKeyInput.value = ''
|
||||||
|
} else {
|
||||||
|
devKeyError.value = t('settings.dev_wrong_key')
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -69,6 +82,37 @@ const paginationItems = [
|
||||||
</div>
|
</div>
|
||||||
<USwitch v-model="showParagraphNumbers" />
|
<USwitch v-model="showParagraphNumbers" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<USeparator />
|
||||||
|
|
||||||
|
<!-- Acceso desarrollador -->
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold text-highlighted mb-1">
|
||||||
|
{{ t('settings.dev_access_title') }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-muted mb-4">{{ t('settings.dev_access_desc') }}</p>
|
||||||
|
|
||||||
|
<div v-if="!unlocked" class="flex gap-2">
|
||||||
|
<UInput
|
||||||
|
v-model="devKeyInput"
|
||||||
|
type="password"
|
||||||
|
:placeholder="t('settings.dev_key_placeholder')"
|
||||||
|
class="flex-1"
|
||||||
|
@keyup.enter="tryUnlock"
|
||||||
|
/>
|
||||||
|
<UButton @click="tryUnlock">
|
||||||
|
{{ t('settings.dev_unlock') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex items-center gap-2">
|
||||||
|
<UIcon name="i-lucide-lock-open" class="size-5 text-green-500" />
|
||||||
|
<span class="text-sm text-green-600 font-medium">{{ t('settings.dev_unlocked') }}</span>
|
||||||
|
<UButton color="neutral" variant="outline" size="sm" @click="lock">
|
||||||
|
{{ t('settings.dev_lock') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
<p v-if="devKeyError" class="text-xs text-red-500 mt-1">{{ devKeyError }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,18 @@ import { useFavoritesStore } from '~/stores/favorites'
|
||||||
import { useSettingsStore } from '~/stores/settings'
|
import { useSettingsStore } from '~/stores/settings'
|
||||||
import type { SearchHit } from '~/types'
|
import type { SearchHit } from '~/types'
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
|
||||||
/* CONFIGURACIÓN — lo único que necesitas tocar */
|
|
||||||
/* -------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
const COLLECTION = 'entrelineas'
|
const COLLECTION = 'entrelineas'
|
||||||
const QUERY_BY = 'text'
|
const QUERY_BY = 'text'
|
||||||
const INCLUDE_FIELDS = '*'
|
const INCLUDE_FIELDS = '*'
|
||||||
const EXTRA_FILTER_BY = ''
|
const EXTRA_FILTER_BY = ''
|
||||||
const FAVORITES_COLLECTION = 'entrelineas'
|
const FAVORITES_COLLECTION = 'entrelineas'
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
|
||||||
/* Estado */
|
|
||||||
/* -------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
const { $i18n } = useNuxtApp()
|
const { $i18n } = useNuxtApp()
|
||||||
const t = $i18n.t
|
const t = $i18n.t
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
|
|
||||||
|
const { unlocked } = useDevMode()
|
||||||
|
|
||||||
const filterBy = computed(() => {
|
const filterBy = computed(() => {
|
||||||
const localeFilter = `locale:=${locale.value}`
|
const localeFilter = `locale:=${locale.value}`
|
||||||
return EXTRA_FILTER_BY ? `${localeFilter} && ${EXTRA_FILTER_BY}` : localeFilter
|
return EXTRA_FILTER_BY ? `${localeFilter} && ${EXTRA_FILTER_BY}` : localeFilter
|
||||||
|
|
@ -33,7 +27,6 @@ const REQUEST_TIMEOUT_MS = 15000
|
||||||
|
|
||||||
const settings = useSettingsStore()
|
const settings = useSettingsStore()
|
||||||
|
|
||||||
// ── Restaurar estado desde URL antes de crear los refs ─────────────────────
|
|
||||||
const { query: q0, page: p0, scroll: s0, selectedId: sid0 } = useSearchUrlState()
|
const { query: q0, page: p0, scroll: s0, selectedId: sid0 } = useSearchUrlState()
|
||||||
|
|
||||||
const query = ref(q0)
|
const query = ref(q0)
|
||||||
|
|
@ -42,6 +35,7 @@ const debouncedQuery = useDebounce(query, 150)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
const errorMsg = ref<string | null>(null)
|
const errorMsg = ref<string | null>(null)
|
||||||
|
const exactSearch = ref(false)
|
||||||
|
|
||||||
interface Study {
|
interface Study {
|
||||||
id?: number
|
id?: number
|
||||||
|
|
@ -56,9 +50,11 @@ interface EntrelineaDoc {
|
||||||
image?: string
|
image?: string
|
||||||
link?: string
|
link?: string
|
||||||
locale?: string
|
locale?: string
|
||||||
origin?: string
|
description?: string
|
||||||
page?: number | string
|
page?: number | string
|
||||||
text?: string
|
text?: string
|
||||||
|
html?: string
|
||||||
|
draft?: boolean
|
||||||
studies?: Study[]
|
studies?: Study[]
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
@ -95,10 +91,6 @@ const hasMore = computed(() =>
|
||||||
settings.paginationType === 'infinite_scroll' ? hits.value.length < total.value : false
|
settings.paginationType === 'infinite_scroll' ? hits.value.length < total.value : false
|
||||||
)
|
)
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
|
||||||
/* Búsqueda */
|
|
||||||
/* -------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
const { documentsApi } = useTypesenseApi()
|
const { documentsApi } = useTypesenseApi()
|
||||||
|
|
||||||
let searchSeq = 0
|
let searchSeq = 0
|
||||||
|
|
@ -130,7 +122,7 @@ async function runSearch(q: string, page = 1, append = false) {
|
||||||
multiSearchSearchesParameter: {
|
multiSearchSearchesParameter: {
|
||||||
searches: [{
|
searches: [{
|
||||||
collection: COLLECTION,
|
collection: COLLECTION,
|
||||||
q: q || '*',
|
q: exactSearch.value && q ? `"${q}"` : q || '*',
|
||||||
queryBy: QUERY_BY,
|
queryBy: QUERY_BY,
|
||||||
includeFields: INCLUDE_FIELDS,
|
includeFields: INCLUDE_FIELDS,
|
||||||
filterBy: filterBy.value,
|
filterBy: filterBy.value,
|
||||||
|
|
@ -198,9 +190,9 @@ watch(debouncedQuery, (q) => {
|
||||||
runSearch(q, 1, false)
|
runSearch(q, 1, false)
|
||||||
})
|
})
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
watch(exactSearch, () => {
|
||||||
/* Selección / panel de detalle */
|
if (query.value.trim()) runSearch(query.value, 1, false)
|
||||||
/* -------------------------------------------------------------------------- */
|
})
|
||||||
|
|
||||||
const selected = ref<EntrelineaDoc | null>(null)
|
const selected = ref<EntrelineaDoc | null>(null)
|
||||||
|
|
||||||
|
|
@ -215,23 +207,17 @@ watch(hits, () => {
|
||||||
if (!stillThere) selected.value = null
|
if (!stillThere) selected.value = null
|
||||||
})
|
})
|
||||||
|
|
||||||
// ID del item seleccionado para sincronizar con la URL
|
|
||||||
const selectedId = computed(() => selected.value?.id?.toString() ?? null)
|
const selectedId = computed(() => selected.value?.id?.toString() ?? null)
|
||||||
|
|
||||||
// Contenedor scrollable de la lista
|
|
||||||
const listEl = ref<HTMLElement | null>(null)
|
const listEl = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
// ── Sincronización con URL ─────────────────────────────────────────────────
|
|
||||||
useSearchUrlSync({ query, page: activePage, selectedId, scrollEl: listEl })
|
useSearchUrlSync({ query, page: activePage, selectedId, scrollEl: listEl })
|
||||||
|
|
||||||
// ── Ciclo de vida ──────────────────────────────────────────────────────────
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await runSearch(q0, p0, false)
|
await runSearch(q0, p0, false)
|
||||||
|
|
||||||
// Restaurar scroll
|
|
||||||
restoreScrollPosition(listEl.value, s0)
|
restoreScrollPosition(listEl.value, s0)
|
||||||
|
|
||||||
// Restaurar item seleccionado
|
|
||||||
if (sid0) {
|
if (sid0) {
|
||||||
const found = hits.value.find(h => h.document.id === sid0)
|
const found = hits.value.find(h => h.document.id === sid0)
|
||||||
if (found) selected.value = found.document
|
if (found) selected.value = found.document
|
||||||
|
|
@ -243,10 +229,6 @@ const isMobile = breakpoints.smaller('lg')
|
||||||
|
|
||||||
useDetailHistory(isPanelOpen, isMobile)
|
useDetailHistory(isPanelOpen, isMobile)
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
|
||||||
/* Favoritos */
|
|
||||||
/* -------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
const favorites = useFavoritesStore()
|
const favorites = useFavoritesStore()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
|
|
@ -255,7 +237,7 @@ function toSearchHit(doc: EntrelineaDoc): SearchHit {
|
||||||
return {
|
return {
|
||||||
_id: id,
|
_id: id,
|
||||||
id,
|
id,
|
||||||
title: id || 'Entrelínea',
|
title: doc.text?.slice(0, 100) || 'Entrelínea',
|
||||||
body: doc.text,
|
body: doc.text,
|
||||||
...doc
|
...doc
|
||||||
}
|
}
|
||||||
|
|
@ -273,17 +255,13 @@ function toggleFavorite(doc: EntrelineaDoc, ev?: Event) {
|
||||||
favorites.toggle(FAVORITES_COLLECTION, toSearchHit(doc))
|
favorites.toggle(FAVORITES_COLLECTION, toSearchHit(doc))
|
||||||
toast.add({
|
toast.add({
|
||||||
title: wasFav ? 'Eliminado de tu lista' : 'Guardado en tu lista',
|
title: wasFav ? 'Eliminado de tu lista' : 'Guardado en tu lista',
|
||||||
description: doc.id,
|
description: doc.text?.slice(0, 100),
|
||||||
icon: wasFav ? 'i-lucide-bookmark-x' : 'i-lucide-bookmark-check',
|
icon: wasFav ? 'i-lucide-bookmark-x' : 'i-lucide-bookmark-check',
|
||||||
color: wasFav ? 'neutral' : 'primary',
|
color: wasFav ? 'neutral' : 'primary',
|
||||||
duration: 1800
|
duration: 1800
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------------------------------- */
|
|
||||||
/* Helpers de presentación */
|
|
||||||
/* -------------------------------------------------------------------------- */
|
|
||||||
|
|
||||||
function highlightedFor(hit: TypesenseHit, field: string): string | null {
|
function highlightedFor(hit: TypesenseHit, field: string): string | null {
|
||||||
const fromArr = hit.highlights?.find(h => h.field === field)
|
const fromArr = hit.highlights?.find(h => h.field === field)
|
||||||
if (fromArr?.snippet) return fromArr.snippet
|
if (fromArr?.snippet) return fromArr.snippet
|
||||||
|
|
@ -307,205 +285,230 @@ function highlightedFor(hit: TypesenseHit, field: string): string | null {
|
||||||
<template #leading>
|
<template #leading>
|
||||||
<UDashboardSidebarCollapse />
|
<UDashboardSidebarCollapse />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #trailing>
|
|
||||||
<UBadge :label="total" variant="subtle" />
|
|
||||||
</template>
|
|
||||||
</UDashboardNavbar>
|
</UDashboardNavbar>
|
||||||
|
|
||||||
<div class="px-4 sm:px-6 py-3 border-b border-default">
|
<!-- Banner: se muestra cuando NO hay clave de desarrollador -->
|
||||||
<UInput
|
<template v-if="!unlocked">
|
||||||
v-model="query"
|
<div class="flex-1 flex items-center justify-center p-8 bg-gradient-to-br from-amber-50 via-white to-amber-100/60">
|
||||||
icon="i-lucide-search"
|
<div class="flex flex-col items-center gap-6 text-center max-w-sm">
|
||||||
:placeholder="t('Search.placeholder')"
|
<div class="size-28 rounded-full bg-gradient-to-br from-amber-300 to-amber-500 flex items-center justify-center shadow-lg shadow-amber-200/50 ring-4 ring-white">
|
||||||
:loading="loading"
|
<UIcon name="i-lucide-flask-conical" class="size-14 text-white" />
|
||||||
size="md"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
<p class="mt-1.5 flex items-center gap-1 text-[11px] text-dimmed">
|
|
||||||
<UIcon name="i-lucide-database" class="size-3" />
|
|
||||||
<span>
|
|
||||||
<code class="text-toned">{{ COLLECTION }}</code>
|
|
||||||
·
|
|
||||||
<code class="text-toned">{{ QUERY_BY }}</code>
|
|
||||||
·
|
|
||||||
<code class="text-toned">{{ filterBy }}</code>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UAlert
|
|
||||||
v-if="errorMsg"
|
|
||||||
:title="errorMsg"
|
|
||||||
color="error"
|
|
||||||
variant="subtle"
|
|
||||||
icon="i-lucide-triangle-alert"
|
|
||||||
class="mx-4 my-2"
|
|
||||||
:actions="[{ label: 'Reintentar', color: 'neutral', variant: 'outline', onClick: retry }]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div ref="listEl" class="overflow-y-auto divide-y divide-default flex-1">
|
|
||||||
<div
|
|
||||||
v-if="loading && !hits.length"
|
|
||||||
class="flex items-center justify-center gap-2 py-16 text-sm text-muted"
|
|
||||||
>
|
|
||||||
<UIcon name="i-lucide-loader-circle" class="size-4 animate-spin" />
|
|
||||||
Buscando...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else-if="!hits.length"
|
|
||||||
class="flex flex-col items-center justify-center gap-2 py-16 text-dimmed text-sm"
|
|
||||||
>
|
|
||||||
<UIcon name="i-lucide-inbox" class="size-10" />
|
|
||||||
<p>{{ query ? `Sin coincidencias para "${query}"` : 'Sin resultados' }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-for="(hit, index) in hits"
|
|
||||||
:key="hit.document.id ?? index"
|
|
||||||
class="p-4 sm:px-6 text-sm cursor-pointer transition-colors border-b-2"
|
|
||||||
:class="[
|
|
||||||
selected && selected.id === hit.document.id
|
|
||||||
? 'bg-primary/10 text-highlighted'
|
|
||||||
: 'border-gray-200 text-toned hover:bg-primary/5'
|
|
||||||
]"
|
|
||||||
@click="selected = hit.document"
|
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between gap-2 mb-1">
|
|
||||||
<div class="min-w-0 flex-1 flex gap-2">
|
|
||||||
<UBadge
|
|
||||||
v-if="hit.document?.studies?.[0]?.date"
|
|
||||||
:label="hit.document?.studies?.[0]?.date"
|
|
||||||
size="sm"
|
|
||||||
variant="subtle"
|
|
||||||
color="info"
|
|
||||||
class="mb-1 uppercase"
|
|
||||||
/>
|
|
||||||
<UBadge
|
|
||||||
v-if="hit.document?.page"
|
|
||||||
:label="`Página ${hit.document?.page}`"
|
|
||||||
size="sm"
|
|
||||||
variant="subtle"
|
|
||||||
color="error"
|
|
||||||
class="mb-1 uppercase"
|
|
||||||
/>
|
|
||||||
<UBadge
|
|
||||||
v-if="hit.document?.filter"
|
|
||||||
:label="hit.document?.filter"
|
|
||||||
size="sm"
|
|
||||||
variant="subtle"
|
|
||||||
color="neutral"
|
|
||||||
class="mb-1 uppercase"
|
|
||||||
/>
|
|
||||||
<UBadge
|
|
||||||
v-if="hit.document?.type"
|
|
||||||
:label="hit.document?.type"
|
|
||||||
size="sm"
|
|
||||||
variant="subtle"
|
|
||||||
color="neutral"
|
|
||||||
class="mb-1 uppercase"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<UTooltip :text="isFav(hit.document) ? 'Quitar de mi lista' : 'Guardar en mi lista'">
|
|
||||||
<UButton
|
|
||||||
:icon="isFav(hit.document) ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
|
|
||||||
:color="isFav(hit.document) ? 'primary' : 'neutral'"
|
|
||||||
variant="ghost"
|
|
||||||
size="xs"
|
|
||||||
:aria-label="isFav(hit.document) ? 'Quitar de mi lista' : 'Guardar en mi lista'"
|
|
||||||
@click="(ev: MouseEvent) => toggleFavorite(hit.document, ev)"
|
|
||||||
/>
|
|
||||||
</UTooltip>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-sm font-semibold tracking-wide truncate mb-2">
|
<div class="space-y-2">
|
||||||
{{ (hit.document?.studies?.[0]?.title as string) || hit.document.id || `entrelinea_${index}` }}
|
<h2 class="text-2xl font-bold text-highlighted">
|
||||||
|
{{ t('entrelineas.development_banner') }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-sm text-toned leading-relaxed">
|
||||||
|
{{ t('entrelineas.development_subtitle_line1') }}<br>
|
||||||
|
{{ t('entrelineas.development_subtitle_line2') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 pt-2">
|
||||||
|
<span class="size-2 rounded-full bg-amber-300 animate-pulse" />
|
||||||
|
<span class="size-2 rounded-full bg-amber-400 animate-pulse" style="animation-delay: 0.2s" />
|
||||||
|
<span class="size-2 rounded-full bg-amber-500 animate-pulse" style="animation-delay: 0.4s" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Funcional: se muestra cuando la clave de desarrollador es correcta -->
|
||||||
|
<template v-else>
|
||||||
|
<div class="px-4 sm:px-6 py-3 border-b border-default">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UInput
|
||||||
|
v-model="query"
|
||||||
|
icon="i-lucide-search"
|
||||||
|
:placeholder="t('Search.placeholder')"
|
||||||
|
:loading="loading"
|
||||||
|
size="md"
|
||||||
|
class="flex-1 min-w-0"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="flex rounded-full p-0.5 shrink-0 transition-colors duration-200"
|
||||||
|
:class="exactSearch ? 'bg-primary' : 'bg-gray-200 dark:bg-gray-700'"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="px-2.5 py-0.5 rounded-full text-xs transition-all duration-200 whitespace-nowrap"
|
||||||
|
:class="!exactSearch ? 'bg-white dark:bg-gray-900 text-gray-900 dark:text-white font-semibold shadow-sm' : 'text-white/40 font-normal'"
|
||||||
|
@click.stop="exactSearch = false"
|
||||||
|
>{{ t('search.word') }}</button>
|
||||||
|
<button
|
||||||
|
class="px-2.5 py-0.5 rounded-full text-xs transition-all duration-200 whitespace-nowrap"
|
||||||
|
:class="exactSearch ? 'bg-white text-primary font-semibold shadow-sm' : 'text-gray-400 dark:text-gray-400 font-normal'"
|
||||||
|
@click.stop="exactSearch = true"
|
||||||
|
>{{ t('search.phrase') }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="mt-1.5 flex items-center gap-1 text-[11px] text-dimmed">
|
||||||
|
<UIcon name="i-lucide-database" class="size-3" />
|
||||||
|
<span>
|
||||||
|
<code class="text-toned">{{ COLLECTION }}</code>
|
||||||
|
·
|
||||||
|
<code class="text-toned">{{ QUERY_BY }}</code>
|
||||||
|
·
|
||||||
|
<code class="text-toned">{{ filterBy }}</code>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<UAlert
|
||||||
|
v-if="errorMsg"
|
||||||
|
:title="errorMsg"
|
||||||
|
color="error"
|
||||||
|
variant="subtle"
|
||||||
|
icon="i-lucide-triangle-alert"
|
||||||
|
class="mx-4 my-2"
|
||||||
|
:actions="[{ label: 'Reintentar', color: 'neutral', variant: 'outline', onClick: retry }]"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div ref="listEl" class="overflow-y-auto divide-y divide-default flex-1">
|
||||||
|
<div
|
||||||
|
v-if="loading && !hits.length"
|
||||||
|
class="flex items-center justify-center gap-2 py-16 text-sm text-muted"
|
||||||
|
>
|
||||||
|
<UIcon name="i-lucide-loader-circle" class="size-4 animate-spin" />
|
||||||
|
Buscando...
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="highlightedFor(hit, 'text') || hit.document.text"
|
v-else-if="!hits.length"
|
||||||
class="snippet-html text-sm text-toned"
|
class="flex flex-col items-center justify-center gap-2 py-16 text-dimmed text-sm"
|
||||||
v-html="highlightedFor(hit, 'text') || hit.document.text"
|
>
|
||||||
/>
|
<UIcon name="i-lucide-inbox" class="size-10" />
|
||||||
|
<p>{{ query ? `Sin coincidencias para "${query}"` : 'Sin resultados' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<USeparator class="my-2"/>
|
<div
|
||||||
|
v-for="(hit, index) in hits"
|
||||||
|
:key="hit.document.id ?? index"
|
||||||
|
class="p-4 sm:px-6 text-sm cursor-pointer transition-colors border-b-2"
|
||||||
|
:class="[
|
||||||
|
selected && selected.id === hit.document.id
|
||||||
|
? 'bg-primary/10 text-highlighted'
|
||||||
|
: 'border-gray-200 text-toned hover:bg-primary/5'
|
||||||
|
]"
|
||||||
|
@click="selected = hit.document"
|
||||||
|
>
|
||||||
|
<div class="flex items-start justify-between gap-2 mb-1">
|
||||||
|
<div class="min-w-0 flex-1 flex gap-2">
|
||||||
|
<UBadge
|
||||||
|
v-if="hit.document?.filter"
|
||||||
|
:label="hit.document?.filter"
|
||||||
|
size="sm"
|
||||||
|
variant="subtle"
|
||||||
|
color="neutral"
|
||||||
|
class="mb-1 uppercase"
|
||||||
|
/>
|
||||||
|
<UBadge
|
||||||
|
v-if="hit.document?.type"
|
||||||
|
:label="hit.document?.type"
|
||||||
|
size="sm"
|
||||||
|
variant="subtle"
|
||||||
|
color="neutral"
|
||||||
|
class="mb-1 uppercase"
|
||||||
|
/>
|
||||||
|
<UBadge
|
||||||
|
v-if="hit.document?.draft"
|
||||||
|
label="Borrador"
|
||||||
|
size="sm"
|
||||||
|
variant="subtle"
|
||||||
|
color="warning"
|
||||||
|
class="mb-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<UTooltip :text="isFav(hit.document) ? 'Quitar de mi lista' : 'Guardar en mi lista'">
|
||||||
|
<UButton
|
||||||
|
:icon="isFav(hit.document) ? 'i-lucide-bookmark-check' : 'i-lucide-bookmark-plus'"
|
||||||
|
:color="isFav(hit.document) ? 'primary' : 'neutral'"
|
||||||
|
variant="ghost"
|
||||||
|
size="xs"
|
||||||
|
:aria-label="isFav(hit.document) ? 'Quitar de mi lista' : 'Guardar en mi lista'"
|
||||||
|
@click="(ev: MouseEvent) => toggleFavorite(hit.document, ev)"
|
||||||
|
/>
|
||||||
|
</UTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="text-xs text-dimmed">
|
<div
|
||||||
{{ hit.document?.origin }}
|
v-if="highlightedFor(hit, 'text') || hit.document.text"
|
||||||
|
class="snippet-html text-sm text-toned"
|
||||||
|
v-html="highlightedFor(hit, 'text') || hit.document.text"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<USeparator class="my-2"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="settings.paginationType === 'infinite_scroll' && hasMore && !loading"
|
||||||
|
class="p-4 flex justify-center"
|
||||||
|
>
|
||||||
|
<UButton
|
||||||
|
variant="outline"
|
||||||
|
color="neutral"
|
||||||
|
size="sm"
|
||||||
|
:loading="loadingMore"
|
||||||
|
@click="loadMore"
|
||||||
|
>
|
||||||
|
Cargar más
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="settings.paginationType === 'infinite_scroll' && hits.length && !hasMore && !loading"
|
||||||
|
class="py-3 text-center text-xs text-dimmed"
|
||||||
|
>
|
||||||
|
No hay más resultados
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Infinite scroll: cargando más -->
|
|
||||||
<div
|
<div
|
||||||
v-if="settings.paginationType === 'infinite_scroll' && hasMore && !loading"
|
v-if="settings.paginationType === 'numbered' && totalPages > 1 && !loading"
|
||||||
class="p-4 flex justify-center"
|
class="px-4 py-3 border-t border-default flex justify-center shrink-0"
|
||||||
>
|
>
|
||||||
<UButton
|
<UPagination
|
||||||
variant="outline"
|
:page="activePage"
|
||||||
color="neutral"
|
:total="total"
|
||||||
|
:items-per-page="settings.pageSize"
|
||||||
size="sm"
|
size="sm"
|
||||||
:loading="loadingMore"
|
@update:page="goToPage"
|
||||||
@click="loadMore"
|
/>
|
||||||
>
|
|
||||||
Cargar más
|
|
||||||
</UButton>
|
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
<div
|
|
||||||
v-else-if="settings.paginationType === 'infinite_scroll' && hits.length && !hasMore && !loading"
|
|
||||||
class="py-3 text-center text-xs text-dimmed"
|
|
||||||
>
|
|
||||||
No hay más resultados
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Paginación numerada -->
|
|
||||||
<div
|
|
||||||
v-if="settings.paginationType === 'numbered' && totalPages > 1 && !loading"
|
|
||||||
class="px-4 py-3 border-t border-default flex justify-center shrink-0"
|
|
||||||
>
|
|
||||||
<UPagination
|
|
||||||
:page="activePage"
|
|
||||||
:total="total"
|
|
||||||
:items-per-page="settings.pageSize"
|
|
||||||
size="sm"
|
|
||||||
@update:page="goToPage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</UDashboardPanel>
|
</UDashboardPanel>
|
||||||
|
|
||||||
<!-- Panel de detalle (escritorio) -->
|
<template v-if="unlocked">
|
||||||
<EntrelineaDetail
|
<EntrelineaDetail
|
||||||
v-if="selected && !isMobile"
|
v-if="selected && !isMobile"
|
||||||
:document="selected"
|
:document="selected"
|
||||||
:collection="FAVORITES_COLLECTION"
|
:collection="FAVORITES_COLLECTION"
|
||||||
:highlighted-text="selected.id ? (hits.find(h => h.document.id === selected!.id)?.highlights?.find(hl => hl.field === 'text')?.value ?? null) : null"
|
:highlighted-text="selected.id ? (hits.find(h => h.document.id === selected!.id)?.highlights?.find(hl => hl.field === 'text')?.value ?? null) : null"
|
||||||
@close="selected = null"
|
@close="selected = null"
|
||||||
/>
|
/>
|
||||||
<div v-else-if="!isMobile" class="hidden lg:flex flex-1 items-center justify-center">
|
<div v-else-if="!isMobile" class="hidden lg:flex flex-1 items-center justify-center">
|
||||||
<div class="flex flex-col items-center gap-2 text-dimmed">
|
<div class="flex flex-col items-center gap-2 text-dimmed">
|
||||||
<UIcon name="i-lucide-mouse-pointer-click" class="size-16" />
|
<UIcon name="i-lucide-mouse-pointer-click" class="size-16" />
|
||||||
<p class="text-sm">
|
<p class="text-sm">
|
||||||
Selecciona una entrelínea para ver el detalle
|
Selecciona una entrelínea para ver el detalle
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Panel de detalle (móvil) -->
|
<ClientOnly>
|
||||||
<ClientOnly>
|
<USlideover v-if="isMobile" v-model:open="isPanelOpen">
|
||||||
<USlideover v-if="isMobile" v-model:open="isPanelOpen">
|
<template #content>
|
||||||
<template #content>
|
<EntrelineaDetail
|
||||||
<EntrelineaDetail
|
v-if="selected"
|
||||||
v-if="selected"
|
:document="selected"
|
||||||
:document="selected"
|
:collection="FAVORITES_COLLECTION"
|
||||||
:collection="FAVORITES_COLLECTION"
|
:highlighted-text="selected.id ? (hits.find(h => h.document.id === selected!.id)?.highlights?.find(hl => hl.field === 'text')?.value ?? null) : null"
|
||||||
:highlighted-text="selected.id ? (hits.find(h => h.document.id === selected!.id)?.highlights?.find(hl => hl.field === 'text')?.value ?? null) : null"
|
@close="selected = null"
|
||||||
@close="selected = null"
|
/>
|
||||||
/>
|
</template>
|
||||||
</template>
|
</USlideover>
|
||||||
</USlideover>
|
</ClientOnly>
|
||||||
</ClientOnly>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,807 +1,13 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
|
|
||||||
import { breakpointsTailwind, useDebounce } from '@vueuse/core'
|
|
||||||
import EstudiosTypensenseDetail from '~/components/estudiosTypensense/EstudiosTypensenseDetail.vue'
|
|
||||||
import { useSettingsStore } from '~/stores/settings'
|
|
||||||
|
|
||||||
const PARAGRAPHS_COLLECTION = 'paragraphs'
|
|
||||||
const DOCUMENTS_COLLECTION = 'documents'
|
|
||||||
const QUERY_BY = 'text'
|
|
||||||
const FAVORITES_COLLECTION = 'bible-studies-ts'
|
|
||||||
|
|
||||||
const { $i18n } = useNuxtApp()
|
|
||||||
const t = $i18n.t
|
|
||||||
const { locale } = useI18n()
|
|
||||||
|
|
||||||
const filterBy = computed(() => `locale:=${locale.value} && type:=activities`)
|
|
||||||
const REQUEST_TIMEOUT_MS = 15000
|
|
||||||
|
|
||||||
const settings = useSettingsStore()
|
|
||||||
|
|
||||||
// ── Restaurar estado desde URL antes de crear los refs ─────────────────────
|
|
||||||
const { query: q0, page: p0, scroll: s0, selectedId: sid0 } = useSearchUrlState()
|
|
||||||
|
|
||||||
const query = ref(q0)
|
|
||||||
const debouncedQuery = useDebounce(query, 150)
|
|
||||||
const loading = ref(false)
|
|
||||||
const loadingMore = ref(false)
|
|
||||||
const errorMsg = ref<string | null>(null)
|
|
||||||
|
|
||||||
// ---- Types ----------------------------------------------------------------
|
|
||||||
|
|
||||||
interface ParagraphDoc {
|
|
||||||
id?: string
|
|
||||||
document_id: string
|
|
||||||
text: string
|
|
||||||
number: number
|
|
||||||
locale: string
|
|
||||||
type: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DocMeta {
|
|
||||||
id: string
|
|
||||||
title: string
|
|
||||||
date?: string
|
|
||||||
timestamp?: number
|
|
||||||
place?: string
|
|
||||||
city?: string
|
|
||||||
state?: string
|
|
||||||
country?: string
|
|
||||||
type?: string
|
|
||||||
slug?: string
|
|
||||||
draft?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DocumentDoc extends DocMeta {
|
|
||||||
code: string
|
|
||||||
locale: string
|
|
||||||
files?: {
|
|
||||||
youtube?: string
|
|
||||||
video?: string
|
|
||||||
audio?: string
|
|
||||||
booklet?: string
|
|
||||||
simple?: string
|
|
||||||
}
|
|
||||||
body?: string
|
|
||||||
[key: string]: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TypesenseHighlight {
|
|
||||||
field?: string
|
|
||||||
snippet?: string
|
|
||||||
value?: string
|
|
||||||
matched_tokens?: string[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TypesenseParagraphHit {
|
|
||||||
document: ParagraphDoc
|
|
||||||
highlights?: TypesenseHighlight[]
|
|
||||||
highlight?: Record<string, { snippet?: string, value?: string }>
|
|
||||||
text_match?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TypesenseGroupedHit {
|
|
||||||
groupKey: string[]
|
|
||||||
hits: TypesenseParagraphHit[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TypesenseSearchResponse {
|
|
||||||
found: number
|
|
||||||
groupedHits?: TypesenseGroupedHit[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SearchGroup {
|
|
||||||
docId: string
|
|
||||||
firstHit: TypesenseParagraphHit
|
|
||||||
allHits: TypesenseParagraphHit[]
|
|
||||||
}
|
|
||||||
|
|
||||||
interface BrowseItem {
|
|
||||||
docId: string
|
|
||||||
meta: DocMeta
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DisplayGroup {
|
|
||||||
docId: string
|
|
||||||
meta: DocMeta | undefined
|
|
||||||
firstHit: TypesenseParagraphHit | null
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- State ----------------------------------------------------------------
|
|
||||||
|
|
||||||
// Modo búsqueda (con query): grupos devueltos por Typesense
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
// Progressive display (sólo en scroll infinito)
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
// Modo exploración (sin query): documentos ordenados por fecha
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
// Grupo unificado para el template
|
|
||||||
const displayGroups = computed((): DisplayGroup[] => {
|
|
||||||
if (!debouncedQuery.value.trim()) {
|
|
||||||
return browseItems.value.map(item => ({
|
|
||||||
docId: item.docId,
|
|
||||||
meta: item.meta,
|
|
||||||
firstHit: null
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
return visibleGroups.value.map(g => ({
|
|
||||||
docId: g.docId,
|
|
||||||
meta: docCache.value[g.docId],
|
|
||||||
firstHit: g.firstHit
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
// Paginación activa (compartida entre browse y search)
|
|
||||||
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))
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cache de metadatos por document_id (sólo modo búsqueda)
|
|
||||||
const docCache = ref<Record<string, DocMeta>>({})
|
|
||||||
|
|
||||||
const { documentsApi } = useTypesenseApi()
|
|
||||||
|
|
||||||
// ---- Batch fetch de metadatos de documentos -------------------------------
|
|
||||||
|
|
||||||
async function fetchDocumentMeta(docIds: string[]) {
|
|
||||||
const unique = docIds.filter(id => id && !(id in docCache.value))
|
|
||||||
console.log('Fetching metadata for documents', unique)
|
|
||||||
try {
|
|
||||||
const res = await documentsApi.multiSearch({
|
|
||||||
multiSearchParameters: {},
|
|
||||||
multiSearchSearchesParameter: {
|
|
||||||
searches: [{
|
|
||||||
collection: DOCUMENTS_COLLECTION,
|
|
||||||
q: '*',
|
|
||||||
queryBy: 'title',
|
|
||||||
filterBy: `id:=[${unique.join(',')}]`,
|
|
||||||
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 multi = await documentsApi.multiSearch({
|
|
||||||
multiSearchParameters: {},
|
|
||||||
multiSearchSearchesParameter: {
|
|
||||||
searches: [{
|
|
||||||
collection: PARAGRAPHS_COLLECTION,
|
|
||||||
q: q || '*',
|
|
||||||
queryBy: QUERY_BY,
|
|
||||||
filterBy: filterBy.value,
|
|
||||||
perPage: settings.pageSize,
|
|
||||||
page: typePage,
|
|
||||||
highlightFullFields: QUERY_BY,
|
|
||||||
highlightFields: QUERY_BY,
|
|
||||||
highlightStartTag: '<mark class="search-match">',
|
|
||||||
highlightEndTag: '</mark>',
|
|
||||||
highlightAffixNumTokens: 30,
|
|
||||||
groupBy: 'document_id'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
console.log('Search response', multi)
|
|
||||||
if (seq !== searchSeq) return
|
|
||||||
|
|
||||||
const res = (multi?.results?.[0] ?? {}) as TypesenseSearchResponse
|
|
||||||
const rawGroups = res?.groupedHits ?? []
|
|
||||||
|
|
||||||
console.log('Raw groups', rawGroups)
|
|
||||||
const newGroups: SearchGroup[] = rawGroups.map(g => ({
|
|
||||||
docId: g.groupKey[0]!,
|
|
||||||
firstHit: g.hits[0]!,
|
|
||||||
allHits: g.hits
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (!append) docCache.value = {}
|
|
||||||
const newDocIds = newGroups.map(g => g.docId).filter(Boolean)
|
|
||||||
await fetchDocumentMeta(newDocIds)
|
|
||||||
|
|
||||||
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: DOCUMENTS_COLLECTION,
|
|
||||||
q: '*',
|
|
||||||
queryBy: 'title',
|
|
||||||
filterBy: filterBy.value,
|
|
||||||
sortBy: 'timestamp:desc',
|
|
||||||
perPage: settings.pageSize,
|
|
||||||
page: typePage,
|
|
||||||
includeFields: 'id,title,date,timestamp,place,city,state,country,type,slug,draft'
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (seq !== searchSeq) return
|
|
||||||
const result = (multi?.results?.[0] as { found?: number, hits?: Array<{ document: DocMeta }> } | undefined)
|
|
||||||
const newItems = (result?.hits ?? []).map(h => ({
|
|
||||||
docId: h.document.id!,
|
|
||||||
meta: h.document
|
|
||||||
}))
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const listContainer = ref<HTMLElement | null>(null)
|
|
||||||
|
|
||||||
function onListScroll() {
|
|
||||||
if (settings.paginationType !== 'infinite_scroll') return
|
|
||||||
const el = listContainer.value
|
|
||||||
if (!el) return
|
|
||||||
if (el.scrollHeight - el.scrollTop - el.clientHeight < 200) {
|
|
||||||
if (!debouncedQuery.value.trim()) {
|
|
||||||
if (hasMoreBrowse.value && !loadingMore.value && !loading.value) {
|
|
||||||
runBrowse(browsePage.value, true)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (hasMoreVisible.value) {
|
|
||||||
visibleGroupCount.value += 10
|
|
||||||
} else if (hasMore.value && !loadingMore.value && !loading.value) {
|
|
||||||
loadMore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function retry() {
|
|
||||||
if (!query.value.trim()) {
|
|
||||||
runBrowse(activePage.value, false)
|
|
||||||
} else {
|
|
||||||
runSearch(query.value, activePage.value, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onBeforeUnmount(() => { if (timeoutId) clearTimeout(timeoutId) })
|
|
||||||
|
|
||||||
watch(debouncedQuery, (q) => {
|
|
||||||
activePage.value = 1
|
|
||||||
if (!q.trim()) {
|
|
||||||
groupedHits.value = []
|
|
||||||
total.value = 0
|
|
||||||
currentPage.value = 1
|
|
||||||
visibleGroupCount.value = 10
|
|
||||||
browseItems.value = []
|
|
||||||
browseTotal.value = 0
|
|
||||||
browsePage.value = 1
|
|
||||||
runBrowse(1, false)
|
|
||||||
} else {
|
|
||||||
browseItems.value = []
|
|
||||||
browseTotal.value = 0
|
|
||||||
browsePage.value = 1
|
|
||||||
groupedHits.value = []
|
|
||||||
total.value = 0
|
|
||||||
currentPage.value = 1
|
|
||||||
visibleGroupCount.value = 10
|
|
||||||
runSearch(q, 1, false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// ---- Selección y carga del detalle ----------------------------------------
|
|
||||||
|
|
||||||
const selectedDocId = ref<string | null>(null)
|
|
||||||
const selectedDocument = ref<DocumentDoc | null>(null)
|
|
||||||
const documentLoading = ref(false)
|
|
||||||
const selectedParagraphs = ref<TypesenseParagraphHit[]>([])
|
|
||||||
const paragraphsLoading = ref(false)
|
|
||||||
|
|
||||||
const selectedHit = ref<TypesenseParagraphHit | null>(null)
|
|
||||||
const selectedMatchingHits = ref<TypesenseParagraphHit[]>([])
|
|
||||||
|
|
||||||
async function fetchFullDocument(docId: string) {
|
|
||||||
documentLoading.value = true
|
|
||||||
selectedDocument.value = null
|
|
||||||
try {
|
|
||||||
const res = await documentsApi.multiSearch({
|
|
||||||
multiSearchParameters: {},
|
|
||||||
multiSearchSearchesParameter: {
|
|
||||||
searches: [{
|
|
||||||
collection: DOCUMENTS_COLLECTION,
|
|
||||||
q: '*',
|
|
||||||
queryBy: 'title',
|
|
||||||
filterBy: `id:=${docId}`,
|
|
||||||
perPage: 1,
|
|
||||||
page: 1
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const docHits = (res?.results?.[0] as { hits?: Array<{ document: DocumentDoc }> })?.hits ?? []
|
|
||||||
selectedDocument.value = docHits[0]?.document ?? null
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching document', err)
|
|
||||||
selectedDocument.value = null
|
|
||||||
} finally {
|
|
||||||
documentLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchDocumentParagraphs(docId: string) {
|
|
||||||
paragraphsLoading.value = true
|
|
||||||
selectedParagraphs.value = []
|
|
||||||
const PER_PAGE = 250
|
|
||||||
let page = 1
|
|
||||||
let totalParagraphs = 0
|
|
||||||
const all: Array<{ document: ParagraphDoc }> = []
|
|
||||||
try {
|
|
||||||
do {
|
|
||||||
const res = await documentsApi.multiSearch({
|
|
||||||
multiSearchParameters: {},
|
|
||||||
multiSearchSearchesParameter: {
|
|
||||||
searches: [{
|
|
||||||
collection: PARAGRAPHS_COLLECTION,
|
|
||||||
q: '*',
|
|
||||||
queryBy: '',
|
|
||||||
filterBy: `document_id:=${docId} && ${filterBy.value}`,
|
|
||||||
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) totalParagraphs = result.found ?? 0
|
|
||||||
all.push(...(result.hits ?? []))
|
|
||||||
page++
|
|
||||||
} while (all.length < totalParagraphs)
|
|
||||||
selectedParagraphs.value = all.map(h => ({ document: h.document }))
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error fetching paragraphs', err)
|
|
||||||
selectedParagraphs.value = []
|
|
||||||
} finally {
|
|
||||||
paragraphsLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function selectGroup(group: DisplayGroup) {
|
|
||||||
selectedDocId.value = group.docId
|
|
||||||
selectedHit.value = group.firstHit
|
|
||||||
const searchGroup = groupedHits.value.find(g => g.docId === group.docId)
|
|
||||||
selectedMatchingHits.value = searchGroup?.allHits ?? []
|
|
||||||
fetchFullDocument(group.docId)
|
|
||||||
fetchDocumentParagraphs(group.docId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isPanelOpen = computed({
|
|
||||||
get() { return !!selectedDocId.value },
|
|
||||||
set(v: boolean) {
|
|
||||||
if (!v) {
|
|
||||||
selectedDocId.value = null
|
|
||||||
selectedDocument.value = null
|
|
||||||
selectedParagraphs.value = []
|
|
||||||
selectedHit.value = null
|
|
||||||
selectedMatchingHits.value = []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(groupedHits, () => {
|
|
||||||
if (!selectedDocId.value || !debouncedQuery.value.trim()) return
|
|
||||||
const still = groupedHits.value.find(g => g.docId === selectedDocId.value)
|
|
||||||
if (!still) {
|
|
||||||
selectedDocId.value = null
|
|
||||||
selectedDocument.value = null
|
|
||||||
selectedParagraphs.value = []
|
|
||||||
selectedHit.value = null
|
|
||||||
selectedMatchingHits.value = []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// ID del documento seleccionado para sincronizar con la URL
|
|
||||||
const selectedId = computed(() => selectedDocId.value)
|
|
||||||
|
|
||||||
// ── Sincronización con URL ─────────────────────────────────────────────────
|
|
||||||
useSearchUrlSync({ query, page: activePage, selectedId, scrollEl: listContainer })
|
|
||||||
|
|
||||||
// ── Ciclo de vida ──────────────────────────────────────────────────────────
|
|
||||||
onMounted(async () => {
|
|
||||||
// Carga inicial: browse si no hay query, search si hay query
|
|
||||||
if (q0.trim()) {
|
|
||||||
await runSearch(q0, p0, false)
|
|
||||||
} else {
|
|
||||||
await runBrowse(p0, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restaurar scroll
|
|
||||||
restoreScrollPosition(listContainer.value, s0)
|
|
||||||
|
|
||||||
// Restaurar documento seleccionado
|
|
||||||
if (sid0) {
|
|
||||||
const group = displayGroups.value.find(g => g.docId === sid0)
|
|
||||||
if (group) {
|
|
||||||
selectGroup(group)
|
|
||||||
} else {
|
|
||||||
// El documento puede no estar en la lista visible (p.ej. browse con paginación)
|
|
||||||
// Lo cargamos directamente por ID
|
|
||||||
selectedDocId.value = sid0
|
|
||||||
fetchFullDocument(sid0)
|
|
||||||
fetchDocumentParagraphs(sid0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const breakpoints = useBreakpoints(breakpointsTailwind)
|
|
||||||
const isMobile = breakpoints.smaller('lg')
|
|
||||||
|
|
||||||
useDetailHistory(isPanelOpen, isMobile)
|
|
||||||
|
|
||||||
// ---- Helpers de presentación ----------------------------------------------
|
|
||||||
|
|
||||||
function highlightedFor(hit: TypesenseParagraphHit, field: string): string | null {
|
|
||||||
const fromArr = hit.highlights?.find(h => h.field === field)
|
|
||||||
if (fromArr?.snippet) return fromArr.snippet
|
|
||||||
if (fromArr?.value) return fromArr.value
|
|
||||||
const fromObj = hit.highlight?.[field]
|
|
||||||
if (fromObj?.snippet) return fromObj.snippet
|
|
||||||
if (fromObj?.value) return fromObj.value
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
function metaDate(meta: DocMeta | undefined): string {
|
|
||||||
if (!meta) return ''
|
|
||||||
const ts = meta.timestamp || (meta.date ? Math.floor(new Date(meta.date).getTime() / 1000) : null)
|
|
||||||
if (!ts) return meta.date || ''
|
|
||||||
return formatDate(ts)
|
|
||||||
}
|
|
||||||
|
|
||||||
function metaLocation(meta: DocMeta | undefined): string {
|
|
||||||
if (!meta) return ''
|
|
||||||
return formatLocation({
|
|
||||||
id: meta.id,
|
|
||||||
date: meta.timestamp ?? 0,
|
|
||||||
slug: meta.slug ?? '',
|
|
||||||
type: meta.type ?? '',
|
|
||||||
place: meta.place ?? '',
|
|
||||||
city: meta.city ?? '',
|
|
||||||
state: meta.state ?? '',
|
|
||||||
country: meta.country ?? '',
|
|
||||||
thumbnail: ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UDashboardPanel
|
<SearchPanel
|
||||||
id="estudios-ts-list"
|
paragraphs-collection="activities_paragraphs"
|
||||||
:default-size="32"
|
main-collection="activities"
|
||||||
:min-size="24"
|
group-by-field="activities_id"
|
||||||
:max-size="45"
|
favorites-collection="bible-studies-ts"
|
||||||
resizable
|
panel-id="estudios-ts-list"
|
||||||
>
|
nav-title-key="nav.bible_studies_ts"
|
||||||
<UDashboardNavbar :title="t('nav.bible_studies_ts')">
|
accent-color="green"
|
||||||
<template #leading>
|
:empty-detail-text="$t('ui.empty_bible_studies')"
|
||||||
<UDashboardSidebarCollapse />
|
:show-draft="true"
|
||||||
</template>
|
|
||||||
<template #trailing>
|
|
||||||
<UBadge :label="displayTotal" variant="subtle" />
|
|
||||||
</template>
|
|
||||||
</UDashboardNavbar>
|
|
||||||
|
|
||||||
<div class="px-4 sm:px-6 py-3 border-b border-default">
|
|
||||||
<UInput
|
|
||||||
v-model="query"
|
|
||||||
icon="i-lucide-search"
|
|
||||||
:placeholder="t('search.placeholder')"
|
|
||||||
:loading="loading"
|
|
||||||
size="md"
|
|
||||||
class="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<UAlert
|
|
||||||
v-if="errorMsg"
|
|
||||||
:title="errorMsg"
|
|
||||||
color="error"
|
|
||||||
variant="subtle"
|
|
||||||
icon="i-lucide-triangle-alert"
|
|
||||||
class="mx-4 my-2"
|
|
||||||
:actions="[{ label: 'Reintentar', color: 'neutral', variant: 'outline', onClick: retry }]"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div ref="listContainer" class="overflow-y-auto divide-y divide-default flex-1" @scroll="onListScroll">
|
|
||||||
<div
|
|
||||||
v-if="loading && !displayGroups.length"
|
|
||||||
class="flex items-center justify-center gap-2 py-16 text-sm text-muted"
|
|
||||||
>
|
|
||||||
<UIcon name="i-lucide-loader-circle" class="size-4 animate-spin" />
|
|
||||||
Buscando...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else-if="!displayGroups.length"
|
|
||||||
class="flex flex-col items-center justify-center gap-2 py-16 text-dimmed text-sm"
|
|
||||||
>
|
|
||||||
<UIcon name="i-lucide-inbox" class="size-10" />
|
|
||||||
<p>{{ query ? `Sin coincidencias para "${query}"` : 'Sin resultados' }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-for="group in displayGroups"
|
|
||||||
:key="group.docId"
|
|
||||||
class="p-4 sm:px-6 text-sm cursor-pointer border-b-2 transition-colors"
|
|
||||||
:class="[
|
|
||||||
selectedDocId === group.docId
|
|
||||||
? 'border-carpagreen bg-carpagreen/10 '
|
|
||||||
: `border-gray-200 hover:border-carpagreen hover:bg-carpagreen/5`
|
|
||||||
]"
|
|
||||||
@click="selectGroup(group)"
|
|
||||||
>
|
|
||||||
<div class="mb-1">
|
|
||||||
<p class="text-sm font-semibold line-clamp-2 text-highlighted ">
|
|
||||||
<UTooltip
|
|
||||||
:text="$t('search.draft')"
|
|
||||||
color="error"
|
|
||||||
>
|
|
||||||
<UIcon
|
|
||||||
name="ph-file-dashed"
|
|
||||||
v-if="group.meta?.draft"
|
|
||||||
class="bg-carpared"
|
|
||||||
/>
|
|
||||||
<!-- <UBadge
|
|
||||||
v-if="group.meta?.draft"
|
|
||||||
label="borrador"
|
|
||||||
variant="solid"
|
|
||||||
color="error"
|
|
||||||
size="xs"
|
|
||||||
class="text-white bg-carpared uppercase font-bold text-xs text-[10px]"
|
|
||||||
/> -->
|
|
||||||
</UTooltip>
|
|
||||||
{{ group.meta?.title || group.docId }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p class="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 text-xs mb-2 text-muted justify-between">
|
|
||||||
<span
|
|
||||||
v-if="metaDate(group.meta)"
|
|
||||||
class="flex items-center gap-1"
|
|
||||||
>
|
|
||||||
<UIcon name="ph:calendar" class="size-4 text-carpagreen" />
|
|
||||||
{{ metaDate(group.meta) }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-if="metaLocation(group.meta)"
|
|
||||||
class="flex items-center gap-1 truncate"
|
|
||||||
>
|
|
||||||
<UIcon name="ph:map-pin" class="size-4 text-carpagreen shrink-0" />
|
|
||||||
<span class="truncate">{{ metaLocation(group.meta) }}</span>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Snippet con highlight: solo en modo búsqueda -->
|
|
||||||
<div
|
|
||||||
v-if="group.firstHit"
|
|
||||||
class="snippet-html text-sm text-dimmed"
|
|
||||||
v-html="highlightedFor(group.firstHit, 'text') || group.firstHit.document.text"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Infinite scroll: cargando más -->
|
|
||||||
<div
|
|
||||||
v-if="settings.paginationType === 'infinite_scroll' && loadingMore"
|
|
||||||
class="flex items-center justify-center gap-2 py-4 text-sm text-muted"
|
|
||||||
>
|
|
||||||
<UIcon name="i-lucide-loader-circle" class="size-4 animate-spin" />
|
|
||||||
Cargando más...
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-else-if="settings.paginationType === 'infinite_scroll' && displayGroups.length && !hasMoreBrowse && !hasMoreVisible && !hasMore && !loading"
|
|
||||||
class="py-3 text-center text-xs text-dimmed"
|
|
||||||
>
|
|
||||||
No hay más resultados
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Paginación numerada -->
|
|
||||||
<div
|
|
||||||
v-if="settings.paginationType === 'numbered' && totalPages > 1 && !loading"
|
|
||||||
class="px-4 py-3 border-t border-default flex justify-center shrink-0"
|
|
||||||
>
|
|
||||||
<UPagination
|
|
||||||
:page="activePage"
|
|
||||||
:total="displayTotal"
|
|
||||||
:items-per-page="settings.pageSize"
|
|
||||||
size="sm"
|
|
||||||
@update:page="goToPage"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</UDashboardPanel>
|
|
||||||
|
|
||||||
<!-- Panel de detalle (escritorio) -->
|
|
||||||
<EstudiosTypensenseDetail
|
|
||||||
v-if="selectedDocId && !isMobile"
|
|
||||||
:document="selectedDocument"
|
|
||||||
:document-loading="documentLoading"
|
|
||||||
:paragraphs="selectedParagraphs"
|
|
||||||
:paragraphs-loading="paragraphsLoading"
|
|
||||||
:collection="FAVORITES_COLLECTION"
|
|
||||||
:query="debouncedQuery"
|
|
||||||
:selected-hit="selectedHit"
|
|
||||||
:selected-matching-hits="selectedMatchingHits"
|
|
||||||
@close="isPanelOpen = false"
|
|
||||||
/>
|
/>
|
||||||
<div v-else-if="!isMobile" class="hidden lg:flex flex-1 items-center justify-center">
|
|
||||||
<div class="flex flex-col items-center gap-2 text-dimmed">
|
|
||||||
<UIcon name="i-lucide-search" class="size-16" />
|
|
||||||
<p class="text-sm">Selecciona una actividad para ver el detalle</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Panel de detalle (móvil) -->
|
|
||||||
<ClientOnly>
|
|
||||||
<USlideover v-if="isMobile" v-model:open="isPanelOpen">
|
|
||||||
<template #content>
|
|
||||||
<EstudiosTypensenseDetail
|
|
||||||
v-if="selectedDocId"
|
|
||||||
:document="selectedDocument"
|
|
||||||
:document-loading="documentLoading"
|
|
||||||
:paragraphs="selectedParagraphs"
|
|
||||||
:paragraphs-loading="paragraphsLoading"
|
|
||||||
:collection="FAVORITES_COLLECTION"
|
|
||||||
:query="debouncedQuery"
|
|
||||||
:selected-hit="selectedHit"
|
|
||||||
:selected-matching-hits="selectedMatchingHits"
|
|
||||||
@close="isPanelOpen = false"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
</USlideover>
|
|
||||||
</ClientOnly>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.snippet-html :deep(p) {
|
|
||||||
display: inline;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.snippet-html :deep(br) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
|
||||||
|
|
@ -5,125 +5,26 @@ 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 { useFavoritesStore, type FavoriteItem } from '~/stores/favorites'
|
import { useFavoritesStore, type FavoriteItem } from '~/stores/favorites'
|
||||||
import EstudiosTypensenseDetail from '~/components/estudiosTypensense/EstudiosTypensenseDetail.vue'
|
import PublicationDetail from '~/components/PublicationDetail.vue'
|
||||||
import EntrelineaDetail from '~/components/entrelineas/EntrelineaDetail.vue'
|
import EntrelineaDetail from '~/components/entrelineas/EntrelineaDetail.vue'
|
||||||
|
|
||||||
const favorites = useFavoritesStore()
|
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 ---------------------------------------------------
|
||||||
|
|
@ -596,7 +495,7 @@ const mobileActions = computed<DropdownMenuItem[][]>(() => [[
|
||||||
@close="selected = null"
|
@close="selected = null"
|
||||||
/>
|
/>
|
||||||
<!-- Resto (actividades, conferencias) → detalle con párrafos de Typesense. -->
|
<!-- Resto (actividades, conferencias) → detalle con párrafos de Typesense. -->
|
||||||
<EstudiosTypensenseDetail
|
<PublicationDetail
|
||||||
v-else-if="selected && !isMobile"
|
v-else-if="selected && !isMobile"
|
||||||
:document="detailDocument"
|
:document="detailDocument"
|
||||||
:document-loading="detailDocumentLoading"
|
:document-loading="detailDocumentLoading"
|
||||||
|
|
@ -623,7 +522,7 @@ const mobileActions = computed<DropdownMenuItem[][]>(() => [[
|
||||||
:collection="selectedCollection"
|
:collection="selectedCollection"
|
||||||
@close="selected = null"
|
@close="selected = null"
|
||||||
/>
|
/>
|
||||||
<EstudiosTypensenseDetail
|
<PublicationDetail
|
||||||
v-else-if="selected"
|
v-else-if="selected"
|
||||||
:document="detailDocument"
|
:document="detailDocument"
|
||||||
:document-loading="detailDocumentLoading"
|
:document-loading="detailDocumentLoading"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,196 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
const t = $i18n.t
|
||||||
|
|
||||||
|
const message = ref('')
|
||||||
|
const sending = ref(false)
|
||||||
|
const sent = ref(false)
|
||||||
|
const error = ref('')
|
||||||
|
const cooldown = ref(0)
|
||||||
|
const honeypot = ref('')
|
||||||
|
const mountTime = Date.now()
|
||||||
|
let cooldownTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
|
||||||
|
const {
|
||||||
|
recaptchaSiteKey: siteKey,
|
||||||
|
feedbackMaxPerHour: MAX_PER_HOUR,
|
||||||
|
feedbackMaxPerSession: MAX_PER_SESSION,
|
||||||
|
feedbackCooldownSec: COOLDOWN_SEC,
|
||||||
|
feedbackMinSeconds: MIN_SECONDS
|
||||||
|
} = useRuntimeConfig().public
|
||||||
|
const RATE_LIMIT_KEY = 'feedback_ratelimit'
|
||||||
|
const SESSION_LIMIT_KEY = 'feedback_session'
|
||||||
|
|
||||||
|
function sanitize(text: string): string {
|
||||||
|
let clean = text
|
||||||
|
clean = clean.replace(/<[^>]*>/g, '')
|
||||||
|
clean = clean.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||||
|
clean = clean.replace(/https?:\/\/[^\s]+/gi, '')
|
||||||
|
clean = clean.replace(/www\.[^\s]+/gi, '')
|
||||||
|
clean = clean.replace(/javascript\s*:/gi, '')
|
||||||
|
clean = clean.replace(/\beval\s*\(/gi, '')
|
||||||
|
clean = clean.replace(/\bon\w+\s*=/gi, '')
|
||||||
|
clean = clean.replace(/[A-Za-z0-9+/]{50,}={0,2}/g, '')
|
||||||
|
clean = clean.replace(/'\s*OR\s+\d+\s*=\s*\d+/gi, '')
|
||||||
|
clean = clean.replace(/'\s*--/g, "'")
|
||||||
|
clean = clean.replace(/'\s*;\s*DROP\s+TABLE/gi, "'")
|
||||||
|
clean = clean.replace(/UNION\s+SELECT/gi, '')
|
||||||
|
clean = clean.replace(/xp_cmdshell/gi, '')
|
||||||
|
clean = clean.replace(/EXEC\s*\(/gi, '')
|
||||||
|
clean = clean.replace(/`[^`]*`/g, '')
|
||||||
|
clean = clean.replace(/\$\(.*?\)/g, '')
|
||||||
|
clean = clean.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g, '')
|
||||||
|
return clean.slice(0, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRateLimitCount(): number {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(RATE_LIMIT_KEY)
|
||||||
|
if (!raw) return 0
|
||||||
|
const data: { count: number; reset: number } = JSON.parse(raw)
|
||||||
|
if (Date.now() > data.reset) {
|
||||||
|
localStorage.removeItem(RATE_LIMIT_KEY)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return data.count
|
||||||
|
} catch {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementRateLimit() {
|
||||||
|
const raw = localStorage.getItem(RATE_LIMIT_KEY)
|
||||||
|
const now = Date.now()
|
||||||
|
let data = { count: 0, reset: now + 3600000 }
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw)
|
||||||
|
if (now <= parsed.reset) {
|
||||||
|
data = parsed
|
||||||
|
}
|
||||||
|
} catch { /* usar default */ }
|
||||||
|
}
|
||||||
|
data.count++
|
||||||
|
localStorage.setItem(RATE_LIMIT_KEY, JSON.stringify(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSessionCount(): number {
|
||||||
|
try {
|
||||||
|
const raw = sessionStorage.getItem(SESSION_LIMIT_KEY)
|
||||||
|
return raw ? Number(raw) : 0
|
||||||
|
} catch {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementSession() {
|
||||||
|
const count = getSessionCount() + 1
|
||||||
|
sessionStorage.setItem(SESSION_LIMIT_KEY, String(count))
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkBlocked(): string | null {
|
||||||
|
const elapsed = (Date.now() - mountTime) / 1000
|
||||||
|
if (elapsed < MIN_SECONDS) return t('feedback.error_cooldown')
|
||||||
|
|
||||||
|
const rateCount = getRateLimitCount()
|
||||||
|
if (rateCount >= MAX_PER_HOUR) return t('feedback.error_rate_limit')
|
||||||
|
|
||||||
|
const sessionCount = getSessionCount()
|
||||||
|
if (sessionCount >= MAX_PER_SESSION) return t('feedback.error_session_limit')
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCooldown() {
|
||||||
|
cooldown.value = COOLDOWN_SEC
|
||||||
|
cooldownTimer = setInterval(() => {
|
||||||
|
cooldown.value--
|
||||||
|
if (cooldown.value <= 0) {
|
||||||
|
if (cooldownTimer) clearInterval(cooldownTimer)
|
||||||
|
cooldown.value = 0
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { executeRecaptcha } = useRecaptcha(siteKey)
|
||||||
|
|
||||||
|
async function send() {
|
||||||
|
const body = sanitize(message.value)
|
||||||
|
if (!body.trim()) return
|
||||||
|
|
||||||
|
if (honeypot.value.trim()) return
|
||||||
|
|
||||||
|
const blocked = checkBlocked()
|
||||||
|
if (blocked !== null) {
|
||||||
|
error.value = blocked
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sending.value = true
|
||||||
|
error.value = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = siteKey ? await executeRecaptcha('submit_feedback') : null
|
||||||
|
|
||||||
|
await $fetch('/api/feedback', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { message: body, recaptchaToken: token }
|
||||||
|
})
|
||||||
|
|
||||||
|
sent.value = true
|
||||||
|
message.value = ''
|
||||||
|
incrementRateLimit()
|
||||||
|
incrementSession()
|
||||||
|
startCooldown()
|
||||||
|
} catch (e) {
|
||||||
|
error.value = t('feedback.error_generic')
|
||||||
|
} finally {
|
||||||
|
sending.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UDashboardPanel id="bugreport-panel" grow>
|
||||||
|
<UDashboardNavbar :title="t('feedback.title')">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<div class="flex-1 flex items-center justify-center p-6">
|
||||||
|
<div v-if="sent" class="text-center space-y-3">
|
||||||
|
<UIcon name="i-lucide-circle-check" class="size-16 text-green-500" />
|
||||||
|
<p class="text-sm text-muted">{{ t('feedback.success_message') }}</p>
|
||||||
|
<UButton color="neutral" variant="outline" @click="sent = false">
|
||||||
|
{{ t('feedback.send_another') }}
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex flex-col items-center gap-6 w-full max-w-xl">
|
||||||
|
<BugReportInput v-model="message" />
|
||||||
|
|
||||||
|
<input
|
||||||
|
v-model="honeypot"
|
||||||
|
type="text"
|
||||||
|
tabindex="-1"
|
||||||
|
autocomplete="off"
|
||||||
|
class="absolute -left-[9999px] -top-[9999px] opacity-0 pointer-events-none size-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<UButton
|
||||||
|
:loading="sending"
|
||||||
|
:disabled="!message.trim() || cooldown > 0"
|
||||||
|
size="lg"
|
||||||
|
class="max-w-xl"
|
||||||
|
@click="send"
|
||||||
|
>
|
||||||
|
{{ sending ? t('feedback.sending') : cooldown > 0 ? t('feedback.wait_seconds', { seconds: cooldown }) : t('feedback.send') }}
|
||||||
|
</UButton>
|
||||||
|
|
||||||
|
<p v-if="error" class="text-sm text-red-500">{{ error }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UDashboardPanel>
|
||||||
|
</template>
|
||||||
|
|
@ -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) => {
|
||||||
|
|
@ -510,7 +527,9 @@ const nearLimit = computed(() => histTotal.value >= Math.floor(HISTORY_LIMIT * 0
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm font-semibold line-clamp-2">
|
<div class="text-sm font-semibold line-clamp-2">
|
||||||
{{ it.hit?.origin || it.hit?.title || 'Sin título' }}
|
{{ it.collection === ENTRELINEAS_COLLECTION
|
||||||
|
? (it.hit?.title || 'Sin título')
|
||||||
|
: (it.hit?.origin || it.hit?.title || 'Sin título') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<UButton
|
<UButton
|
||||||
|
|
@ -546,11 +565,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 +593,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>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,101 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
// Home redirects straight to the search experience.
|
// Home redirects straight to the search experience.
|
||||||
definePageMeta({
|
import type { ButtonProps } from '@nuxt/ui'
|
||||||
middleware: () => navigateTo('/estudios-biblicos', { redirectCode: 302 })
|
|
||||||
})
|
const nuxtApp = useNuxtApp()
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
const t = $i18n.t
|
||||||
|
|
||||||
|
const release = releases[0]
|
||||||
|
|
||||||
|
const links = ref<ButtonProps[]>([
|
||||||
|
{
|
||||||
|
label: t('nav.tour'),
|
||||||
|
icon: 'ph-student',
|
||||||
|
color: 'error',
|
||||||
|
class: 'hidden sm:flex',
|
||||||
|
onClick: () => nuxtApp.callHook('tour',11)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('nav.bible_studies'),
|
||||||
|
to: `/${$i18n.locale.value}/estudios-biblicos`,
|
||||||
|
icon: 'ph-books',
|
||||||
|
color: 'primary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('nav.conferences'),
|
||||||
|
to: `/${$i18n.locale.value}/conferencias`,
|
||||||
|
icon: 'ph-books',
|
||||||
|
color: 'secondary'
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div />
|
<UDashboardPanel id="changelog-panel" grow>
|
||||||
|
<UDashboardNavbar :title="t('nav.home')">
|
||||||
|
<template #leading>
|
||||||
|
<UDashboardSidebarCollapse />
|
||||||
|
</template>
|
||||||
|
</UDashboardNavbar>
|
||||||
|
|
||||||
|
<div class="flex-1 overflow-y-auto">
|
||||||
|
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-8 sm:py-12">
|
||||||
|
<div class="flex flex-col lg:flex-row lg:items-start gap-8 lg:gap-12">
|
||||||
|
|
||||||
|
<!-- Hero: título, descripción y accesos -->
|
||||||
|
<div class="flex-1 flex flex-col gap-6">
|
||||||
|
<UBadge variant="subtle" color="neutral" size="sm" class="w-fit">
|
||||||
|
{{ release?.date }}
|
||||||
|
</UBadge>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-bold text-highlighted tracking-tight">
|
||||||
|
Buscador Carpa
|
||||||
|
</h1>
|
||||||
|
<p class="mt-3 text-base text-muted leading-relaxed">
|
||||||
|
{{ $t('home.instructions') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col sm:flex-row flex-wrap gap-3">
|
||||||
|
<UButton
|
||||||
|
v-for="link in links"
|
||||||
|
:key="link.label"
|
||||||
|
v-bind="link"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Changelog card -->
|
||||||
|
<div id="index_changelog" class="w-full lg:w-80 xl:w-96 shrink-0">
|
||||||
|
<UCard variant="soft">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<UIcon name="ph-git-branch" class="size-4 text-muted shrink-0" />
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="font-semibold text-highlighted text-sm truncate">{{ release?.title }}</p>
|
||||||
|
<p v-if="release?.description" class="text-xs text-muted truncate">{{ release?.description }}</p>
|
||||||
|
</div>
|
||||||
|
<UBadge variant="outline" color="neutral" size="xs" class="shrink-0">
|
||||||
|
v{{ release?.version }}
|
||||||
|
</UBadge>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<ul class="space-y-2">
|
||||||
|
<li v-for="(change, i) in release?.changes" :key="i" class="flex items-start gap-2 text-sm">
|
||||||
|
<UBadge :color="typeConfig[change.type].color as any" variant="subtle" size="xs" class="mt-0.5 shrink-0">
|
||||||
|
{{ typeConfig[change.type].label }}
|
||||||
|
</UBadge>
|
||||||
|
<span class="text-default leading-snug">{{ change.text }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</UDashboardPanel>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,16 @@ export interface Member {
|
||||||
avatar: AvatarProps
|
avatar: AvatarProps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
grecaptcha: {
|
||||||
|
ready: (callback: () => void) => void
|
||||||
|
execute: (siteKey: string, options: { action: string }) => Promise<string>
|
||||||
|
render: (container: string | HTMLElement, options: Record<string, unknown>) => void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface Notification {
|
export interface Notification {
|
||||||
id: number
|
id: number
|
||||||
unread?: boolean
|
unread?: boolean
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,131 @@
|
||||||
|
export const typeConfig: Record<ChangeEntry['type'], { label: string; color: string }> = {
|
||||||
|
nuevo: { label: 'Nuevo', color: 'success' },
|
||||||
|
mejora: { label: 'Mejora', color: 'info' },
|
||||||
|
fix: { label: 'Fix', color: 'warning' },
|
||||||
|
eliminado: { label: 'Eliminado', color: 'error' }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChangeEntry {
|
||||||
|
type: 'nuevo' | 'mejora' | 'fix' | 'eliminado'
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Release {
|
||||||
|
version: string
|
||||||
|
date: string
|
||||||
|
title: string
|
||||||
|
description?: string
|
||||||
|
changes: ChangeEntry[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const releases: Release[] = [
|
||||||
|
{
|
||||||
|
version: '0.7',
|
||||||
|
date: '31 de mayo, 2026 11:50PM',
|
||||||
|
title: 'Tour y optimizaciones',
|
||||||
|
changes: [
|
||||||
|
{ type: 'nuevo', text: 'Agregado tour virtual con localizacion para explicar funcionamiento del buscador'},
|
||||||
|
{ type: 'nuevo', text: 'Agregada pagina de inicio que muestra la version mas reciente del changelog' },
|
||||||
|
{ type: 'mejora', text: 'Separacion de changelog a un TS aparte para utilizar en changelog y en el home sin duplicacion de codigo'},
|
||||||
|
{ type: 'nuevo', text: 'Finalizado flow de automatizacion entre backend y typesense'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '0.6',
|
||||||
|
date: '31 de mayo, 2026',
|
||||||
|
title: 'Feedback con traducciones, bloqueo de Entrelíneas y acceso desarrollador',
|
||||||
|
changes: [
|
||||||
|
{ type: 'nuevo', text: 'Página de Feedback con traducciones completas en 4 idiomas' },
|
||||||
|
{ type: 'nuevo', text: 'Sistema de bloqueo por clave de desarrollador para secciones en desarrollo' },
|
||||||
|
{ type: 'nuevo', text: 'Acceso de desarrollador en Configuración con desbloqueo por clave' },
|
||||||
|
{ type: 'nuevo', text: 'Banner visual mejorado para Entrelíneas cuando está bloqueado' },
|
||||||
|
{ type: 'mejora', text: 'Traducciones añadidas al componente BugReportInput' },
|
||||||
|
{ type: 'mejora', text: 'Nombre del tab Feedback ahora usa traducciones' },
|
||||||
|
{ type: 'mejora', text: 'Textos del banner de Entrelíneas traducidos a 4 idiomas' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '0.5',
|
||||||
|
date: '30 de mayo, 2026',
|
||||||
|
title: 'Soporte de HTML en Entrelíneas',
|
||||||
|
description: 'El visor de Entrelíneas ahora renderiza contenido en HTML además de texto plano.',
|
||||||
|
changes: [
|
||||||
|
{ type: 'nuevo', text: 'Renderizado de contenido HTML en el detalle de Entrelíneas' },
|
||||||
|
{ type: 'mejora', text: 'Los fragmentos HTML se muestran con formato original preservado' },
|
||||||
|
{ type: 'fix', text: 'Corrección de errores de sintaxis en el componente de detalle' },
|
||||||
|
{ type: 'mejora', text: 'El historial ahora muestra correctamente documentos de Entrelíneas con HTML' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '0.4',
|
||||||
|
date: '26 de mayo, 2026',
|
||||||
|
title: 'Mejoras de interfaz y exploracion',
|
||||||
|
changes: [
|
||||||
|
{ type: 'mejora', text: 'El código del documento aparece visible en la pantalla de detalle' },
|
||||||
|
{ type: 'mejora', text: 'El badge "Borrador" solo se muestra cuando el documento es borrador' },
|
||||||
|
{ type: 'mejora', text: 'Tooltip mejorado al copiar párrafos al portapapeles' },
|
||||||
|
{ type: 'mejora', text: 'El explorador filtra resultados mostrando solo ítems con párrafos' },
|
||||||
|
{ type: 'mejora', text: 'Panel lateral preparado para notas y resaltados (próximamente)' },
|
||||||
|
{ type: 'fix', text: 'Posicionamiento del panel lateral corregido en documentos cortos' },
|
||||||
|
{ type: 'fix', text: 'Corrección de traducciones en francés' },
|
||||||
|
{ type: 'fix', text: 'Ajustes visuales en múltiples vistas' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '0.3',
|
||||||
|
date: '25 de mayo, 2026',
|
||||||
|
title: 'Portapapeles y corrección de bugs',
|
||||||
|
changes: [
|
||||||
|
{ type: 'nuevo', text: 'Panel de copiado mejorado al seleccionar párrafos' },
|
||||||
|
{ type: 'mejora', text: 'Función de copiar al portapapeles más robusta e independiente' },
|
||||||
|
{ type: 'fix', text: 'Corrección de bug al guardar y mostrar favoritos' },
|
||||||
|
{ type: 'fix', text: 'Corrección de bug en la carga de referencias' },
|
||||||
|
{ type: 'fix', text: 'Corrección de bug en el detalle de publicaciones de referencia' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '0.2',
|
||||||
|
date: '21–24 de mayo, 2026',
|
||||||
|
title: 'Conferencias, copiar párrafos y estabilidad',
|
||||||
|
changes: [
|
||||||
|
{ type: 'nuevo', text: 'Soporte de documentos de Conferencias y Actividades' },
|
||||||
|
{ type: 'nuevo', text: 'Clic en párrafo para copiarlo directamente al portapapeles' },
|
||||||
|
{ type: 'nuevo', text: 'Zoom de imagen en el visor de Entrelíneas' },
|
||||||
|
{ type: 'mejora', text: 'Número de párrafo más discreto en el detalle del documento' },
|
||||||
|
{ type: 'mejora', text: 'Publicación del documento visible en todas las vistas y traducida' },
|
||||||
|
{ type: 'mejora', text: 'Corrección de estilos visuales para contenido antiguo' },
|
||||||
|
{ type: 'fix', text: 'Corrección de bug en favoritos (detalle)' },
|
||||||
|
{ type: 'fix', text: 'Badge "Borrador" agregado en lista y detalle de Estudios Bíblicos' },
|
||||||
|
{ type: 'fix', text: 'Rutas de navegación migradas correctamente a nuevo motor de búsqueda' },
|
||||||
|
{ type: 'fix', text: 'Corrección de scroll infinito en móvil para Entrelíneas' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '0.1',
|
||||||
|
date: '10–12 de mayo, 2026',
|
||||||
|
title: 'Historial, Favoritos y multilengua',
|
||||||
|
description: 'Primera versión funcional con las secciones principales activas.',
|
||||||
|
changes: [
|
||||||
|
{ type: 'nuevo', text: 'Historial de búsqueda y exploración' },
|
||||||
|
{ type: 'nuevo', text: 'Favoritos: guarda y accede a documentos desde Mi Listado' },
|
||||||
|
{ type: 'nuevo', text: 'Selector de idioma: Español, English, Français, Português' },
|
||||||
|
{ type: 'nuevo', text: 'Cantidad aproximada de resultados visible en los listados' },
|
||||||
|
{ type: 'mejora', text: 'Paginación configurable: scroll infinito o páginas numeradas' },
|
||||||
|
{ type: 'mejora', text: 'Ajuste de cantidad de resultados por página en Configuración' },
|
||||||
|
{ type: 'fix', text: 'Corrección de traducciones en el menú principal' },
|
||||||
|
{ type: 'fix', text: 'Ajustes de colores y badges en listas de resultados' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
version: '0.0.1',
|
||||||
|
date: '7 de mayo, 2026',
|
||||||
|
title: 'Lanzamiento inicial',
|
||||||
|
description: 'Primera versión del buscador con Estudios Bíblicos y Entrelíneas.',
|
||||||
|
changes: [
|
||||||
|
{ type: 'nuevo', text: 'Búsqueda en Estudios Bíblicos' },
|
||||||
|
{ type: 'nuevo', text: 'Visor de Entrelíneas con imagen y texto' },
|
||||||
|
{ type: 'nuevo', text: 'Búsqueda por palabras o por frase exacta' },
|
||||||
|
{ type: 'nuevo', text: 'Estructura base de la aplicación' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -1,171 +1,220 @@
|
||||||
import dayjs from "dayjs";
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
export interface ItemObject {
|
export interface ItemObject {
|
||||||
id: string;
|
id: string
|
||||||
date: number;
|
date: number
|
||||||
slug: string;
|
timestamp: number
|
||||||
type: string;
|
slug: string
|
||||||
place: string;
|
type: string
|
||||||
city: string;
|
place: string
|
||||||
state: string;
|
city: string
|
||||||
country: string;
|
state: string
|
||||||
thumbnail: string;
|
country: string
|
||||||
|
thumbnail: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FilesObject {
|
export interface FilesObject {
|
||||||
youtube?: string;
|
youtube?: string
|
||||||
video?: string;
|
video?: string
|
||||||
audio?: string;
|
audio?: string
|
||||||
booklet?: string;
|
booklet?: string
|
||||||
simple?: string;
|
simple?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatFiles( files:FilesObject ){
|
export function formatFiles(files: FilesObject) {
|
||||||
const { $i18n } = useNuxtApp();
|
const { $i18n } = useNuxtApp()
|
||||||
const t = $i18n.t;
|
const t = $i18n.t
|
||||||
|
|
||||||
let items = []
|
const items = []
|
||||||
if (files) {
|
if (files) {
|
||||||
if (files.youtube) {
|
if (files.youtube) {
|
||||||
items.push({
|
items.push({
|
||||||
to: files.youtube,
|
to: files.youtube,
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
label: 'Youtube',
|
label: 'Youtube',
|
||||||
icon: 'ph:youtube-logo-thin',
|
icon: 'ph:youtube-logo-thin',
|
||||||
labelClass: 'text-xs',
|
labelClass: 'text-xs'
|
||||||
})
|
})
|
||||||
}
|
|
||||||
if (files.video) {
|
|
||||||
items.push({
|
|
||||||
to: files.video,
|
|
||||||
target: '_blank',
|
|
||||||
label: t('Activities.video'),
|
|
||||||
icon: 'ph:file-video-thin',
|
|
||||||
labelClass: 'text-xs'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (files.audio) {
|
|
||||||
items.push({
|
|
||||||
to: `https://actividadeswp.carpa.com/wp-content/uploads/${files.audio}`,
|
|
||||||
target: '_blank',
|
|
||||||
label: t('Activities.audio'),
|
|
||||||
icon: 'ph:file-audio-thin',
|
|
||||||
labelClass: 'text-xs'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (files.booklet) {
|
|
||||||
items.push({
|
|
||||||
to: files.booklet,
|
|
||||||
target: '_blank',
|
|
||||||
label: t('Activities.book'),
|
|
||||||
icon: 'ph:notebook-thin',
|
|
||||||
labelClass: 'text-xs'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (files.simple) {
|
|
||||||
items.push({
|
|
||||||
to: files.simple,
|
|
||||||
target: '_blank',
|
|
||||||
label: t('Activities.simple'),
|
|
||||||
icon: 'ph:note-thin',
|
|
||||||
labelClass: 'text-xs'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return items
|
if (files.video) {
|
||||||
}
|
items.push({
|
||||||
|
to: files.video,
|
||||||
export function formatDate( d:number ){
|
target: '_blank',
|
||||||
const { $i18n } = useNuxtApp();
|
label: t('downloads.video'),
|
||||||
const locale = $i18n.locale;
|
icon: 'ph:file-video-thin',
|
||||||
|
labelClass: 'text-xs'
|
||||||
let date = new Date(d * 1000)
|
})
|
||||||
|
|
||||||
let dateString = date.toLocaleString(locale.value, { year: 'numeric', month:'long', day: 'numeric', weekday:'long', timeZone:'UTC' })
|
|
||||||
return capitalizeFirstLetter( dateString );
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatLocation( i: ItemObject){
|
|
||||||
const { $i18n } = useNuxtApp();
|
|
||||||
const locale = $i18n.locale;
|
|
||||||
const regionNames = new Intl.DisplayNames(
|
|
||||||
[ locale.value], {type: 'region'}
|
|
||||||
);
|
|
||||||
|
|
||||||
var locationParts = [];
|
|
||||||
locationParts.push(i?.place);
|
|
||||||
locationParts.push(i?.city);
|
|
||||||
locationParts.push(i?.state);
|
|
||||||
if( i.country ){
|
|
||||||
locationParts.push(regionNames.of(i.country));
|
|
||||||
}
|
}
|
||||||
|
if (files.audio) {
|
||||||
|
let fileUrl = ''
|
||||||
|
if (files.audio.startsWith('http')) {
|
||||||
|
fileUrl = files.audio
|
||||||
|
} else {
|
||||||
|
fileUrl = `https://actividadeswp.carpa.com/wp-content/uploads/${files.audio}`
|
||||||
|
}
|
||||||
|
|
||||||
return locationParts.filter(Boolean).join(', ');
|
items.push({
|
||||||
}
|
to: fileUrl,
|
||||||
|
target: '_blank',
|
||||||
export function getDay( d:number ){
|
label: t('downloads.audio'),
|
||||||
const { $i18n } = useNuxtApp();
|
icon: 'ph:file-audio-thin',
|
||||||
const locale = $i18n.locale;
|
labelClass: 'text-xs'
|
||||||
let date = new Date(d * 1000);
|
})
|
||||||
|
|
||||||
return date.toLocaleString(locale.value, { weekday: 'long', timeZone: 'utc' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDayDate( d:number ){
|
|
||||||
const { $i18n } = useNuxtApp();
|
|
||||||
const locale = $i18n.locale;
|
|
||||||
let date = new Date(d * 1000);
|
|
||||||
|
|
||||||
return date.toLocaleString(locale.value, { day: 'numeric', timeZone: 'utc' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getMonth( d:number ){
|
|
||||||
const { $i18n } = useNuxtApp();
|
|
||||||
const locale = $i18n.locale;
|
|
||||||
let date = new Date(d * 1000);
|
|
||||||
|
|
||||||
return date.toLocaleString(locale.value, { month: 'long', timeZone: 'utc' });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setUrl( item:ItemObject, isSearch:boolean ) {
|
|
||||||
const { $i18n } = useNuxtApp();
|
|
||||||
const locale = $i18n.locale;
|
|
||||||
const date = dayjs(item.date*1000);
|
|
||||||
const month = (date.month()+1).toString()
|
|
||||||
const slug = (item.slug || item.slug === 'undefined' ? item.slug : item.id);
|
|
||||||
|
|
||||||
if( isSearch ){
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
return `/${locale.value}/${item.type}/${date.year()}/${month.padStart(2, '0')}/${slug}`;
|
|
||||||
}
|
}
|
||||||
|
if (files.booklet) {
|
||||||
|
let fileUrl = ''
|
||||||
|
if (files.booklet.startsWith('http')) {
|
||||||
|
fileUrl = files.booklet
|
||||||
|
} else {
|
||||||
|
fileUrl = `https://actividadeswp.carpa.com/wp-content/uploads/${files.booklet}`
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
to: fileUrl,
|
||||||
|
target: '_blank',
|
||||||
|
label: t('downloads.book'),
|
||||||
|
icon: 'ph:notebook-thin',
|
||||||
|
labelClass: 'text-xs'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (files.simple) {
|
||||||
|
let fileUrl = ''
|
||||||
|
if (files.simple.startsWith('http')) {
|
||||||
|
fileUrl = files.simple
|
||||||
|
} else {
|
||||||
|
fileUrl = `https://actividadeswp.carpa.com/wp-content/uploads/${files.simple}`
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
to: fileUrl,
|
||||||
|
target: '_blank',
|
||||||
|
label: t('downloads.simple'),
|
||||||
|
icon: 'ph:note-thin',
|
||||||
|
labelClass: 'text-xs'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getThumbnail( item:ItemObject ) {
|
export function formatDate(d: number) {
|
||||||
console.log("ITEM",item);
|
if (!d) {
|
||||||
let path = item.thumbnail
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
const locale = $i18n.locale
|
||||||
|
|
||||||
|
const date = new Date(d * 1000)
|
||||||
|
|
||||||
|
const dateString = date.toLocaleString(locale.value, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
weekday: 'long',
|
||||||
|
timeZone: 'UTC'
|
||||||
|
})
|
||||||
|
return capitalizeFirstLetter(dateString)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatLocation(i: ItemObject) {
|
||||||
|
if (!i) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
const locale = $i18n.locale
|
||||||
|
const regionNames = new Intl.DisplayNames([locale.value], { type: 'region' })
|
||||||
|
|
||||||
|
const locationParts = []
|
||||||
|
locationParts.push(i?.place)
|
||||||
|
locationParts.push(i?.city)
|
||||||
|
locationParts.push(i?.state)
|
||||||
|
if (i.country) {
|
||||||
|
locationParts.push(regionNames.of(i.country))
|
||||||
|
}
|
||||||
|
|
||||||
|
return locationParts.filter(Boolean).join(', ')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatSignature(i: ItemObject) {
|
||||||
|
const date = formatDate(i?.timestamp)
|
||||||
|
const location = formatLocation(i)
|
||||||
|
if (date === undefined || location === undefined) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const val = `${date} - ${location}`
|
||||||
|
|
||||||
|
if (!val) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDay(d: number) {
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
const locale = $i18n.locale
|
||||||
|
const date = new Date(d * 1000)
|
||||||
|
|
||||||
|
return date.toLocaleString(locale.value, {
|
||||||
|
weekday: 'long',
|
||||||
|
timeZone: 'utc'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDayDate(d: number) {
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
const locale = $i18n.locale
|
||||||
|
const date = new Date(d * 1000)
|
||||||
|
|
||||||
|
return date.toLocaleString(locale.value, { day: 'numeric', timeZone: 'utc' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMonth(d: number) {
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
const locale = $i18n.locale
|
||||||
|
const date = new Date(d * 1000)
|
||||||
|
|
||||||
|
return date.toLocaleString(locale.value, { month: 'long', timeZone: 'utc' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUrl(item: ItemObject, isSearch: boolean) {
|
||||||
|
const { $i18n } = useNuxtApp()
|
||||||
|
const locale = $i18n.locale
|
||||||
|
const date = dayjs(item.date * 1000)
|
||||||
|
const month = (date.month() + 1).toString()
|
||||||
|
const slug = item.slug || item.slug === 'undefined' ? item.slug : item.id
|
||||||
|
|
||||||
|
if (isSearch) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return `/${locale.value}/${item.type}/${date.year()}/${month.padStart(2, '0')}/${slug}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getThumbnail(item: ItemObject) {
|
||||||
|
const path = item.thumbnail
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return "https://images.carpa.com/tr:w-900,f-auto/youtube_thumbnail_46396.png"
|
return 'https://images.carpa.com/tr:w-900,f-auto/youtube_thumbnail_46396.png'
|
||||||
} else {
|
} else {
|
||||||
return `https://images.carpa.com/${item.type}/${path}?tr=w-900`
|
return `https://images.carpa.com/${item.type}/${path}?tr=w-900`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAuthor( type:string ){
|
export function getAuthor(type: string) {
|
||||||
let author = ''
|
let author = ''
|
||||||
switch (type){
|
switch (type) {
|
||||||
case "activities":
|
case 'activities':
|
||||||
author = 'Dr. José Benjamín Pérez Matos'
|
author = 'Dr. José Benjamín Pérez Matos'
|
||||||
break
|
break
|
||||||
case "conferences":
|
case 'conferences':
|
||||||
author = 'Dr. William Soto Santiago'
|
author = 'Dr. William Soto Santiago'
|
||||||
break
|
break
|
||||||
case "sermons":
|
case 'sermons':
|
||||||
author = 'William Marrion Branham';
|
author = 'William Marrion Branham'
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
return author
|
return author
|
||||||
}
|
}
|
||||||
|
|
||||||
// removed: openItem relied on a Pinia store that is not installed in this app.
|
// removed: openItem relied on a Pinia store that is not installed in this app.
|
||||||
// Detail navigation is now handled directly inside the search pages.
|
// Detail navigation is now handled directly inside the search pages.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/* Copy to Clipboard */
|
/* Copy to Clipboard */
|
||||||
export async function copyToClipboard() {
|
export async function copyToClipboard(doc: ItemObject) {
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
|
|
||||||
const selection = window.getSelection()
|
const selection = window.getSelection()
|
||||||
|
|
@ -8,8 +8,8 @@ export async function copyToClipboard() {
|
||||||
const range = selection?.getRangeAt(0)
|
const range = selection?.getRangeAt(0)
|
||||||
container.appendChild(range.cloneContents())
|
container.appendChild(range.cloneContents())
|
||||||
|
|
||||||
const htmlOutput = container.innerHTML
|
const htmlOutput = container.innerHTML + '<br/>' + doc.title + '<br/>' + formatSignature(doc)
|
||||||
const textOutput = selection?.toString()
|
const textOutput = selection?.toString() + '\n\n' + doc.title + '\n' + formatSignature(doc)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Modern Clipboard API requires ClipboardItem
|
// Modern Clipboard API requires ClipboardItem
|
||||||
|
|
@ -19,63 +19,34 @@ export async function copyToClipboard() {
|
||||||
})
|
})
|
||||||
|
|
||||||
await navigator.clipboard.write([clipboardItem])
|
await navigator.clipboard.write([clipboardItem])
|
||||||
|
|
||||||
window.getSelection()?.removeAllRanges()
|
|
||||||
|
|
||||||
toast.add({
|
toast.add({
|
||||||
title: 'Texto copiado!',
|
title: 'Texto copiado!',
|
||||||
description: `${textOutput}`,
|
description: `${truncateAtWord(textOutput, 150)}`,
|
||||||
icon: 'ph-clipboard-text',
|
icon: 'ph-clipboard-text',
|
||||||
color: 'success'
|
color: doc.type == 'activities' ? 'success' : 'info'
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy: ', err)
|
console.error('Failed to copy: ', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Function to copy url with search parameters into clipboard */
|
const truncateAtWord = (str: string, limit: number) => {
|
||||||
export function copyParagraphToUrl(id: string) {
|
if (str.length <= limit) return str
|
||||||
console.log('Encoding id', id)
|
const subString = str.slice(0, limit)
|
||||||
|
return subString.slice(0, subString.lastIndexOf('')) + '...'
|
||||||
|
}
|
||||||
|
|
||||||
const highlights = {
|
export function debugDocument(json: []) {
|
||||||
highlights: [
|
let data = ''
|
||||||
{
|
// Iterate through keys and values
|
||||||
id: id,
|
for (const key in json) {
|
||||||
color: '#f90'
|
if (json.hasOwnProperty(key)) {
|
||||||
}
|
data += `${key}: ${json[key]}\n\r`
|
||||||
]
|
}
|
||||||
}
|
}
|
||||||
|
return data
|
||||||
const blob = encodeObjectToBase64Url(highlights)
|
|
||||||
|
|
||||||
console.log('Encoded highlights', blob)
|
|
||||||
|
|
||||||
console.log('Decoded highlights object', base64UrlDecode(blob))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Encode JS object to base64Url compatible string */
|
export function addNote() {
|
||||||
function encodeObjectToBase64Url(obj: object) {
|
return 'yay'
|
||||||
// 1. Convert object to JSON string
|
|
||||||
const jsonString = JSON.stringify(obj)
|
|
||||||
|
|
||||||
// 2. Base64 encode the JSON string
|
|
||||||
const base64 = btoa(jsonString)
|
|
||||||
|
|
||||||
// 3. Make it URL-safe and remove padding
|
|
||||||
return base64
|
|
||||||
.replace(/\+/g, '-')
|
|
||||||
.replace(/\//g, '_')
|
|
||||||
.replace(/=+$/, '')
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Decode base64URL String to json object */
|
|
||||||
function base64UrlDecode(base64Url: string) {
|
|
||||||
// Replace URL-safe characters and add padding
|
|
||||||
let base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
||||||
while (base64.length % 4 !== 0) {
|
|
||||||
base64 += '='
|
|
||||||
}
|
|
||||||
return decodeURIComponent(atob(base64).split('').map(function (c) {
|
|
||||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
|
|
||||||
}).join(''))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
meta {
|
||||||
|
name: Listar todas las collections
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/collections
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
X-TYPESENSE-API-KEY: {{adminApiKey}}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
meta {
|
||||||
|
name: activities_paragraphs
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/collections/activities_paragraphs
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
X-TYPESENSE-API-KEY: {{adminApiKey}}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
meta {
|
||||||
|
name: conferences_paragraphs
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: {{baseUrl}}/collections/conferences_paragraphs
|
||||||
|
body: none
|
||||||
|
auth: none
|
||||||
|
}
|
||||||
|
|
||||||
|
headers {
|
||||||
|
X-TYPESENSE-API-KEY: {{adminApiKey}}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
meta {
|
||||||
|
name: Collections
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"name": "Typesense - LGCC Search",
|
||||||
|
"type": "collection",
|
||||||
|
"ignore": []
|
||||||
|
}
|
||||||
49
lang/en.json
49
lang/en.json
|
|
@ -9,9 +9,12 @@
|
||||||
"between_the_lines": "Between the Lines",
|
"between_the_lines": "Between the Lines",
|
||||||
"my_list": "My List",
|
"my_list": "My List",
|
||||||
"history": "History",
|
"history": "History",
|
||||||
"settings": "Settings"
|
"settings": "Settings",
|
||||||
|
"changelog": "What's New"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
"word": "Word",
|
||||||
|
"phrase": "Phrase",
|
||||||
"placeholder": "Search for...",
|
"placeholder": "Search for...",
|
||||||
"searching": "Searching...",
|
"searching": "Searching...",
|
||||||
"tip": "Tip: wrap in \"quotes\" for exact phrase in that order.",
|
"tip": "Tip: wrap in \"quotes\" for exact phrase in that order.",
|
||||||
|
|
@ -44,7 +47,45 @@
|
||||||
"infinite_scroll_desc": "Results load automatically when you reach the end of the list.",
|
"infinite_scroll_desc": "Results load automatically when you reach the end of the list.",
|
||||||
"numbered": "Numbered pages",
|
"numbered": "Numbered pages",
|
||||||
"numbered_desc": "Navigate between pages with pagination controls.",
|
"numbered_desc": "Navigate between pages with pagination controls.",
|
||||||
"paragraph_numbers_title": "Paragraph numbers",
|
"paragraph_numbers_title": "Paragraph numbers",
|
||||||
"paragraph_numbers_desc": "Show the paragraph number next to each paragraph in the document detail."
|
"paragraph_numbers_desc": "Show the paragraph number next to each paragraph in the document detail.",
|
||||||
}
|
"dev_access_title": "Developer access",
|
||||||
|
"dev_access_desc": "Enter the key to enable sections in development.",
|
||||||
|
"dev_key_placeholder": "Development key",
|
||||||
|
"dev_unlock": "Unlock",
|
||||||
|
"dev_lock": "Lock",
|
||||||
|
"dev_unlocked": "Unlocked",
|
||||||
|
"dev_wrong_key": "Wrong key"
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"audio": "Audio",
|
||||||
|
"book": "Book",
|
||||||
|
"simple": "Simple"
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"title": "Report an error",
|
||||||
|
"description": "If you find an error or have a suggestion to improve the search, write it here. Your feedback helps us improve.",
|
||||||
|
"placeholder": "Describe the error or suggestion...",
|
||||||
|
"anonymous_notice": "Anonymous report. Do not include personal information or passwords.",
|
||||||
|
"success_message": "Message sent successfully. Thank you for your feedback.",
|
||||||
|
"send_another": "Send another",
|
||||||
|
"send": "Send report",
|
||||||
|
"sending": "Sending...",
|
||||||
|
"wait_seconds": "Wait {seconds}s",
|
||||||
|
"error_generic": "Could not send the message. Try again.",
|
||||||
|
"error_cooldown": "Wait a few seconds before sending.",
|
||||||
|
"error_rate_limit": "You have reached the hourly limit. Try again later.",
|
||||||
|
"error_session_limit": "You have reached the session limit."
|
||||||
|
},
|
||||||
|
"entrelineas": {
|
||||||
|
"development_banner": "This section is under development",
|
||||||
|
"development_subtitle_line1": "We're preparing details for this section.",
|
||||||
|
"development_subtitle_line2": "Come back soon."
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"copy": "Copy",
|
||||||
|
"draft": "Draft",
|
||||||
|
"empty_bible_studies": "Choose a Bible Study to see the detail",
|
||||||
|
"empty_conferences": "Choose a Conference to see the detail"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
70
lang/es.json
70
lang/es.json
|
|
@ -8,7 +8,29 @@
|
||||||
"between_the_lines": "Entrelíneas",
|
"between_the_lines": "Entrelíneas",
|
||||||
"my_list": "Mi Listado",
|
"my_list": "Mi Listado",
|
||||||
"history": "Historial",
|
"history": "Historial",
|
||||||
"settings": "Configuración"
|
"settings": "Configuración",
|
||||||
|
"changelog": "Novedades",
|
||||||
|
"tour": "Toma el tour"
|
||||||
|
},
|
||||||
|
"tour": {
|
||||||
|
"progress": "{current} de {total}",
|
||||||
|
"next": "Siguiente",
|
||||||
|
"prev": "Anterior",
|
||||||
|
"done": "Finalizar",
|
||||||
|
"bible_studies_description": "Realiza busquedas en los estudios biblicos predicados por el Dr. José Benjamín Pérez",
|
||||||
|
"conferences_description": "Realiza busquedas en las conferencias predicadas por el Dr. William Soto Santiago",
|
||||||
|
"betweenthelines_description": "Realiza busquedas en las imagenes de entrelineas de los estudios del Dr. José Benjamín Perez",
|
||||||
|
"favorites_description": "Listado de resultados guardados como favoritos para facil acceso futuro",
|
||||||
|
"history_description": "Historial de los resultados de busqueda que has visto",
|
||||||
|
"settings_description": "Configuracion, cantidad de resultados por pagina, tipo de paginacion, entre otros",
|
||||||
|
"changelog_description": "Bitacora de cambios realizados al sitio en orden cronologico",
|
||||||
|
"feedback_description": "Tienes alguna sugerencia o queja? realizala aqui",
|
||||||
|
"localeselector_description": "Cambia facilmente el idioma de la pagina",
|
||||||
|
"index_changelog": "Panel de ultimos cambios subidos",
|
||||||
|
"favorites_toggle": "Boton para guardar / quitar este documento de mis favoritos"
|
||||||
|
},
|
||||||
|
"home": {
|
||||||
|
"instructions": "Bienvenidos, aqui podran buscar entre los estudios biblicos, las conferencias y las entrelineas que estan disponibles en el material de archivo de La Gran Carpa Catedral."
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "Buscar...",
|
"placeholder": "Buscar...",
|
||||||
|
|
@ -27,6 +49,8 @@
|
||||||
"hits_per_page": "aciertos por página",
|
"hits_per_page": "aciertos por página",
|
||||||
"hits_retrieved_in": "aciertos logrados en",
|
"hits_retrieved_in": "aciertos logrados en",
|
||||||
"for": "Buscando",
|
"for": "Buscando",
|
||||||
|
"word": "Palabra",
|
||||||
|
"phrase": "Frase",
|
||||||
"words": "palabras",
|
"words": "palabras",
|
||||||
"phrases": "frases",
|
"phrases": "frases",
|
||||||
"words_tooltip": "Buscar por palabras",
|
"words_tooltip": "Buscar por palabras",
|
||||||
|
|
@ -50,6 +74,48 @@
|
||||||
"numbered": "Páginas numeradas",
|
"numbered": "Páginas numeradas",
|
||||||
"numbered_desc": "Navega entre páginas con controles de paginación.",
|
"numbered_desc": "Navega entre páginas con controles de paginación.",
|
||||||
"paragraph_numbers_title": "Números de párrafo",
|
"paragraph_numbers_title": "Números de párrafo",
|
||||||
"paragraph_numbers_desc": "Muestra el número de párrafo al lado de cada párrafo en el detalle del documento."
|
"paragraph_numbers_desc": "Muestra el número de párrafo al lado de cada párrafo en el detalle del documento.",
|
||||||
|
"dev_access_title": "Acceso de desarrollador",
|
||||||
|
"dev_access_desc": "Ingresa la clave para habilitar secciones en desarrollo.",
|
||||||
|
"dev_key_placeholder": "Clave de desarrollo",
|
||||||
|
"dev_unlock": "Desbloquear",
|
||||||
|
"dev_lock": "Bloquear",
|
||||||
|
"dev_unlocked": "Desbloqueado",
|
||||||
|
"dev_wrong_key": "Clave incorrecta"
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"audio": "Audio",
|
||||||
|
"book": "Libro",
|
||||||
|
"simple": "Sencillo",
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"title": "Reportar un error",
|
||||||
|
"description": "Si encuentras un error o tienes una sugerencia para mejorar el buscador, escríbelo aquí. Tu retroalimentación nos ayuda a mejorar.",
|
||||||
|
"placeholder": "Describe el error o sugerencia...",
|
||||||
|
"anonymous_notice": "Reporte anónimo. No incluyas información personal ni contraseñas.",
|
||||||
|
"success_message": "Mensaje enviado correctamente. Gracias por tu retroalimentación.",
|
||||||
|
"send_another": "Enviar otro",
|
||||||
|
"send": "Enviar reporte",
|
||||||
|
"sending": "Enviando...",
|
||||||
|
"wait_seconds": "Espera {seconds}s",
|
||||||
|
"error_generic": "No se pudo enviar el mensaje. Intenta de nuevo.",
|
||||||
|
"error_cooldown": "Espera unos segundos antes de enviar.",
|
||||||
|
"error_rate_limit": "Has alcanzado el límite de envíos por hora. Intenta más tarde.",
|
||||||
|
"error_session_limit": "Has alcanzado el límite de envíos en esta sesión."
|
||||||
|
},
|
||||||
|
"entrelineas": {
|
||||||
|
"development_banner": "Esta sección se encuentra en desarrollo",
|
||||||
|
"development_subtitle_line1": "Estamos preparando detalles para esta sección.",
|
||||||
|
"development_subtitle_line2": "Vuelve pronto."
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"copy": "Copiar",
|
||||||
|
"draft": "Borrador",
|
||||||
|
"empty_bible_studies": "Selecciona un Estudio Bíblico para ver el detalle",
|
||||||
|
"empty_conferences": "Selecciona una Conferencia para ver el detalle"
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"title": "La Gran Carpa Catedral - Buscador",
|
||||||
|
"description": "Buscador de actividades y conferencias."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
45
lang/fr.json
45
lang/fr.json
|
|
@ -9,9 +9,12 @@
|
||||||
"between_the_lines": "Entre les lignes",
|
"between_the_lines": "Entre les lignes",
|
||||||
"my_list": "Ma liste",
|
"my_list": "Ma liste",
|
||||||
"history": "Historique",
|
"history": "Historique",
|
||||||
"settings": "Paramètres"
|
"settings": "Paramètres",
|
||||||
|
"changelog": "Nouveautés"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
"word": "Mot",
|
||||||
|
"phrase": "Phrase",
|
||||||
"placeholder": "Rechercher des activités",
|
"placeholder": "Rechercher des activités",
|
||||||
"publication": "Publicación",
|
"publication": "Publicación",
|
||||||
"draft": ""
|
"draft": ""
|
||||||
|
|
@ -33,6 +36,44 @@
|
||||||
"numbered": "Pages numérotées",
|
"numbered": "Pages numérotées",
|
||||||
"numbered_desc": "Naviguez entre les pages avec des contrôles de pagination.",
|
"numbered_desc": "Naviguez entre les pages avec des contrôles de pagination.",
|
||||||
"paragraph_numbers_title": "Numéros de paragraphe",
|
"paragraph_numbers_title": "Numéros de paragraphe",
|
||||||
"paragraph_numbers_desc": "Affiche le numéro de paragraphe à côté de chaque paragraphe dans le détail du document."
|
"paragraph_numbers_desc": "Affiche le numéro de paragraphe à côté de chaque paragraphe dans le détail du document.",
|
||||||
|
"dev_access_title": "Accès développeur",
|
||||||
|
"dev_access_desc": "Entrez la clé pour activer les sections en développement.",
|
||||||
|
"dev_key_placeholder": "Clé de développement",
|
||||||
|
"dev_unlock": "Déverrouiller",
|
||||||
|
"dev_lock": "Verrouiller",
|
||||||
|
"dev_unlocked": "Déverrouillé",
|
||||||
|
"dev_wrong_key": "Clé incorrecte"
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"audio": "Audio",
|
||||||
|
"book": "Libre",
|
||||||
|
"simple": "Simple"
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"title": "Rapporter une erreur",
|
||||||
|
"description": "Si vous trouvez une erreur ou avez une suggestion pour améliorer la recherche, écrivez-la ici. Votre retour nous aide à nous améliorer.",
|
||||||
|
"placeholder": "Décrivez l'erreur ou la suggestion...",
|
||||||
|
"anonymous_notice": "Rapport anonyme. N'incluez pas d'informations personnelles ni de mots de passe.",
|
||||||
|
"success_message": "Message envoyé avec succès. Merci pour votre retour.",
|
||||||
|
"send_another": "Envoyer un autre",
|
||||||
|
"send": "Envoyer le rapport",
|
||||||
|
"sending": "Envoi en cours...",
|
||||||
|
"wait_seconds": "Attendez {seconds}s",
|
||||||
|
"error_generic": "Impossible d'envoyer le message. Réessayez.",
|
||||||
|
"error_cooldown": "Attendez quelques secondes avant d'envoyer.",
|
||||||
|
"error_rate_limit": "Vous avez atteint la limite horaire. Réessayez plus tard.",
|
||||||
|
"error_session_limit": "Vous avez atteint la limite de cette session."
|
||||||
|
},
|
||||||
|
"entrelineas": {
|
||||||
|
"development_banner": "Cette section est en cours de développement",
|
||||||
|
"development_subtitle_line1": "Nous préparons les détails de cette section.",
|
||||||
|
"development_subtitle_line2": "Revenez bientôt."
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"copy": "Copie",
|
||||||
|
"draft": "Brouillon",
|
||||||
|
"empty_bible_studies": "Choisissez une étude pour voir le détail",
|
||||||
|
"empty_conferences": "Choisissez une conférence pour voir le détail"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
43
lang/pt.json
43
lang/pt.json
|
|
@ -9,9 +9,12 @@
|
||||||
"between_the_lines": "Entre as linhas",
|
"between_the_lines": "Entre as linhas",
|
||||||
"my_list": "Minha lista",
|
"my_list": "Minha lista",
|
||||||
"history": "Registro",
|
"history": "Registro",
|
||||||
"settings": "Configurações"
|
"settings": "Configurações",
|
||||||
|
"changelog": "Novidades"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
|
"word": "Palavra",
|
||||||
|
"phrase": "Frase",
|
||||||
"placeholder": "Digite para pesquisar...",
|
"placeholder": "Digite para pesquisar...",
|
||||||
"publication": "Publicação",
|
"publication": "Publicação",
|
||||||
"draft": "Borrador",
|
"draft": "Borrador",
|
||||||
|
|
@ -43,6 +46,42 @@
|
||||||
"numbered": "Páginas numeradas",
|
"numbered": "Páginas numeradas",
|
||||||
"numbered_desc": "Navegue entre páginas com controles de paginação.",
|
"numbered_desc": "Navegue entre páginas com controles de paginação.",
|
||||||
"paragraph_numbers_title": "Números de parágrafo",
|
"paragraph_numbers_title": "Números de parágrafo",
|
||||||
"paragraph_numbers_desc": "Exibe o número do parágrafo ao lado de cada parágrafo no detalhe do documento."
|
"paragraph_numbers_desc": "Exibe o número do parágrafo ao lado de cada parágrafo no detalhe do documento.",
|
||||||
|
"dev_access_title": "Acesso de desenvolvedor",
|
||||||
|
"dev_access_desc": "Insira a chave para ativar seções em desenvolvimento.",
|
||||||
|
"dev_key_placeholder": "Chave de desenvolvimento",
|
||||||
|
"dev_unlock": "Desbloquear",
|
||||||
|
"dev_lock": "Bloquear",
|
||||||
|
"dev_unlocked": "Desbloqueado",
|
||||||
|
"dev_wrong_key": "Chave incorreta"
|
||||||
|
},
|
||||||
|
"downloads": {
|
||||||
|
"audio": "Áudio",
|
||||||
|
"book": "Livro",
|
||||||
|
"simple": "Simples"
|
||||||
|
},
|
||||||
|
"feedback": {
|
||||||
|
"title": "Reportar um erro",
|
||||||
|
"description": "Se encontrar um erro ou tem uma sugestão, escreva-la aqui. A sua opinião ira ajudar-nos a melhorar.",
|
||||||
|
"placeholder": "Descreva o erro ou sugestão",
|
||||||
|
"anonymous_notice": "Reporte anônimo. Não inclua informações pessoais nem senhas.",
|
||||||
|
"success_message": "Mensagem enviada corretamente. Obrigado por sua opinião.",
|
||||||
|
"send_another": "Enviar outro",
|
||||||
|
"send": "Enviar reporte",
|
||||||
|
"sending": "Enviando...",
|
||||||
|
"wait_seconds": "Aguarde {seconds}s",
|
||||||
|
"error_generic": "Não foi possível enviar a mensagem. Tente novamente.",
|
||||||
|
"error_cooldown": "Aguarde alguns segundos antes de enviar.",
|
||||||
|
"error_rate_limit": "Você atingiu o limite horário. Tente novamente mais tarde.",
|
||||||
|
"error_session_limit": "Você atingiu o limite desta sessão."
|
||||||
|
},
|
||||||
|
"entrelineas": {
|
||||||
|
"development_banner": "Esta seção está em desenvolvimento",
|
||||||
|
"development_subtitle_line1": "Estamos preparando detalhes para esta seleção.",
|
||||||
|
"development_subtitle_line2": "Volte logo."
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"copy": "Cópia",
|
||||||
|
"draft": "Rascunho"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
modules: ['@nuxt/eslint', '@nuxt/ui', '@vueuse/nuxt', 'nuxt-meilisearch', '@nuxtjs/i18n', '@pinia/nuxt', '@sfxcode/nuxt-typesense'],
|
modules: ['@nuxt/eslint', '@nuxt/ui', '@vueuse/nuxt', '@nuxtjs/i18n', '@pinia/nuxt', '@sfxcode/nuxt-typesense','nuxt-driver.js'],
|
||||||
|
|
||||||
|
app: {
|
||||||
|
head: {
|
||||||
|
htmlAttrs: { translate: 'no' },
|
||||||
|
meta: [{ name: 'google', content: 'notranslate' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
devtools: {
|
devtools: {
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
@ -12,6 +19,19 @@ export default defineNuxtConfig({
|
||||||
colorMode: false
|
colorMode: false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
runtimeConfig: {
|
||||||
|
feedbackToken: process.env.NUXT_FEEDBACK_TOKEN || '',
|
||||||
|
public: {
|
||||||
|
feedbackWebhook: process.env.NUXT_PUBLIC_FEEDBACK_WEBHOOK || '',
|
||||||
|
recaptchaSiteKey: process.env.NUXT_PUBLIC_RECAPTCHA_SITE_KEY || '',
|
||||||
|
feedbackMaxPerHour: Number(process.env.NUXT_PUBLIC_FEEDBACK_MAX_PER_HOUR) || 5,
|
||||||
|
feedbackMaxPerSession: Number(process.env.NUXT_PUBLIC_FEEDBACK_MAX_PER_SESSION) || 3,
|
||||||
|
feedbackCooldownSec: Number(process.env.NUXT_PUBLIC_FEEDBACK_COOLDOWN_SEC) || 45,
|
||||||
|
feedbackMinSeconds: Number(process.env.NUXT_PUBLIC_FEEDBACK_MIN_SECONDS) || 4,
|
||||||
|
entrelineasDevKey: process.env.NUXT_PUBLIC_ENTRELINEAS_DEV_KEY || ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
routeRules: {
|
routeRules: {
|
||||||
'/api/**': {
|
'/api/**': {
|
||||||
cors: true
|
cors: true
|
||||||
|
|
@ -68,18 +88,9 @@ export default defineNuxtConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
meilisearch: {
|
|
||||||
hostUrl: 'https://search.carpa.com', // required
|
|
||||||
searchApiKey: '04be59c1f633e2bb434082fc1a6fcc6ce97e3630e3fcf9e814e1f03a386c03e1', // required
|
|
||||||
serverSideUsage: true // default: false
|
|
||||||
},
|
|
||||||
|
|
||||||
typesense: {
|
typesense: {
|
||||||
url: 'https://searchts.carpa.com', // Your Typesense server URL
|
url: process.env.NUXT_PUBLIC_TYPESENSE_URL || 'https://searchts.carpa.com',
|
||||||
apiKey: 'a2lbIMTxh48KVteLLndpBfo4tuOIGiwD', // Your Typesense API key
|
apiKey: process.env.NUXT_PUBLIC_TYPESENSE_API_KEY || '',
|
||||||
// Habilita los composables auto-importados en cliente
|
|
||||||
// (useTypesenseDocuments, useTypesenseApi, etc.).
|
|
||||||
// ⚠️ Solo usa una clave de búsqueda (search-only) aquí: queda expuesta al navegador.
|
|
||||||
clientMode: true
|
clientMode: true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"nuxt": "^4.4.2",
|
"nuxt": "^4.4.2",
|
||||||
"nuxt-meilisearch": "^1.4.17",
|
"nuxt-driver.js": "^0.1.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"scule": "^1.3.0",
|
"scule": "^1.3.0",
|
||||||
"tailwindcss": "^4.2.4",
|
"tailwindcss": "^4.2.4",
|
||||||
|
|
@ -39,267 +39,6 @@
|
||||||
"vue-tsc": "^3.2.7"
|
"vue-tsc": "^3.2.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ai-sdk/gateway": {
|
|
||||||
"version": "2.0.86",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-2.0.86.tgz",
|
|
||||||
"integrity": "sha512-pP9F5G7C5sqZtAwquFB+g50lVS/s4Wf/ll2WSm0ODk0Iix27trVgBDpFK5CBletcQXSDlAvSQQi25nBodYLt3g==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@ai-sdk/provider": "2.0.3",
|
|
||||||
"@ai-sdk/provider-utils": "3.0.25",
|
|
||||||
"@vercel/oidc": "3.1.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"zod": "^3.25.76 || ^4.1.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ai-sdk/provider": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.3.tgz",
|
|
||||||
"integrity": "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"json-schema": "^0.4.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@ai-sdk/provider-utils": {
|
|
||||||
"version": "3.0.25",
|
|
||||||
"resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.25.tgz",
|
|
||||||
"integrity": "sha512-CvsRu+32Y8a167s+lrIBtsybvgTHp8j9y+6BeTvLeoW3Q+okw/b4CnNUFOLIXsRaKHQKAH+IHNJPYWywfpw0LA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@ai-sdk/provider": "2.0.3",
|
|
||||||
"@standard-schema/spec": "^1.0.0",
|
|
||||||
"eventsource-parser": "^3.0.6"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"zod": "^3.25.76 || ^4.1.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/abtesting": {
|
|
||||||
"version": "1.18.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.18.0.tgz",
|
|
||||||
"integrity": "sha512-8siuLG+FIns1AjZ/g2SDVwHz9S+ObacDQISEJvS8XsNei1zl3FXqfqQrBpmrG7ACWCyesXHbicMJtvRbg00FEw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/client-common": "5.52.0",
|
|
||||||
"@algolia/requester-browser-xhr": "5.52.0",
|
|
||||||
"@algolia/requester-fetch": "5.52.0",
|
|
||||||
"@algolia/requester-node-http": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/client-abtesting": {
|
|
||||||
"version": "5.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.52.0.tgz",
|
|
||||||
"integrity": "sha512-wtwPgyPmO7b7sQPVgoK29c1VpfS08DnnJCmxX/oU1pV2DlMRJCzQcLN7JSloYpodyKHwM8+9wOzlAM0co3TDmA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/client-common": "5.52.0",
|
|
||||||
"@algolia/requester-browser-xhr": "5.52.0",
|
|
||||||
"@algolia/requester-fetch": "5.52.0",
|
|
||||||
"@algolia/requester-node-http": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/client-analytics": {
|
|
||||||
"version": "5.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.52.0.tgz",
|
|
||||||
"integrity": "sha512-9KY36bRl4AH7RjqSeDDOKnjsz4IxQFBEOB8/fWmEbdQe+Isbs5jGzVJu9NEPQ1Tgwxlf8Uf07Swj3jZyMNUZ2g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/client-common": "5.52.0",
|
|
||||||
"@algolia/requester-browser-xhr": "5.52.0",
|
|
||||||
"@algolia/requester-fetch": "5.52.0",
|
|
||||||
"@algolia/requester-node-http": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/client-common": {
|
|
||||||
"version": "5.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.52.0.tgz",
|
|
||||||
"integrity": "sha512-3a/qM3dzJqqfTx7Yrw7uGQ98I3Q0rDfb4Vkv0wEzko96l7YQMxfBVz/VbLq2N+c59GweYv6Vhp8mPeqnWJSITw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/client-insights": {
|
|
||||||
"version": "5.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.52.0.tgz",
|
|
||||||
"integrity": "sha512-Rki7ACbMcvbQW0BuM84x9dkGHY47ABmv4jU6tYssat2k02p3mIUms2YOLUAMeknhmnFsj6lb6ZzOXdMWMyc1sA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/client-common": "5.52.0",
|
|
||||||
"@algolia/requester-browser-xhr": "5.52.0",
|
|
||||||
"@algolia/requester-fetch": "5.52.0",
|
|
||||||
"@algolia/requester-node-http": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/client-personalization": {
|
|
||||||
"version": "5.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.52.0.tgz",
|
|
||||||
"integrity": "sha512-96s4Uzc3kk+/f4jJXIVVGWP5XlngOGNQ1x6hW9AT59pOixHlOs5tqJg+ZUS/GQ6h/iYP0ceQcmxDQeLyCLTaDQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/client-common": "5.52.0",
|
|
||||||
"@algolia/requester-browser-xhr": "5.52.0",
|
|
||||||
"@algolia/requester-fetch": "5.52.0",
|
|
||||||
"@algolia/requester-node-http": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/client-query-suggestions": {
|
|
||||||
"version": "5.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.52.0.tgz",
|
|
||||||
"integrity": "sha512-lqeycNpSPe5Qa0OUWpejVvYQjQWV5nQuLT0a4aq7XzRAvCxprV/6Lf841EygdD2nrFnuS58ok7Au1uOtXzpnkg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/client-common": "5.52.0",
|
|
||||||
"@algolia/requester-browser-xhr": "5.52.0",
|
|
||||||
"@algolia/requester-fetch": "5.52.0",
|
|
||||||
"@algolia/requester-node-http": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/client-search": {
|
|
||||||
"version": "5.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.52.0.tgz",
|
|
||||||
"integrity": "sha512-ly1wETVGRo30cx61O7fetESN+ElL9c9K+bD/AVgnT1ar4c6v+/Yqjrhdtu6Fm4D0s4NZP081Isf6tunH1wUXHg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/client-common": "5.52.0",
|
|
||||||
"@algolia/requester-browser-xhr": "5.52.0",
|
|
||||||
"@algolia/requester-fetch": "5.52.0",
|
|
||||||
"@algolia/requester-node-http": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/events": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/ingestion": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.52.0.tgz",
|
|
||||||
"integrity": "sha512-U4EeTvgmluRjj39ykZSAd5X+a6LD5m7/mcOWDmB7hqm1R6QY0yT8jLxpNVEjYhzgEN5hcDGW6X67EWQY8KiYGQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/client-common": "5.52.0",
|
|
||||||
"@algolia/requester-browser-xhr": "5.52.0",
|
|
||||||
"@algolia/requester-fetch": "5.52.0",
|
|
||||||
"@algolia/requester-node-http": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/monitoring": {
|
|
||||||
"version": "1.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.52.0.tgz",
|
|
||||||
"integrity": "sha512-FCPnDcILfpTE94u7BVlV4DmnSV5wE3+j25EEF+3dYPrVzkVCSoAHs318oWDGxnxsAgiL4HpL12Jc4XHmw9shpA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/client-common": "5.52.0",
|
|
||||||
"@algolia/requester-browser-xhr": "5.52.0",
|
|
||||||
"@algolia/requester-fetch": "5.52.0",
|
|
||||||
"@algolia/requester-node-http": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/recommend": {
|
|
||||||
"version": "5.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.52.0.tgz",
|
|
||||||
"integrity": "sha512-br3DO7n4N8CXwTRbZS0MnB4WQ9YHfNjCwkCEzVR/wek/qNTDQKDb0nROmkFaNZ8ucUqUVKZi074dbwMwRDlK8Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/client-common": "5.52.0",
|
|
||||||
"@algolia/requester-browser-xhr": "5.52.0",
|
|
||||||
"@algolia/requester-fetch": "5.52.0",
|
|
||||||
"@algolia/requester-node-http": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/requester-browser-xhr": {
|
|
||||||
"version": "5.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.52.0.tgz",
|
|
||||||
"integrity": "sha512-b0T/Ca2c9KyEslKsVrGZvbe1UrrKKSdfXhBZ2pbpKahFUzJfziRZ0urbOm7V65O0tO/jwU+Lo/+bIiiyhzGt8w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/client-common": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/requester-fetch": {
|
|
||||||
"version": "5.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.52.0.tgz",
|
|
||||||
"integrity": "sha512-ozBT8J/mtD4H4IAojw8QPirlcL2gHrI1BGuZ4/ZXXO/rTE1yQ4VIPJj4mTTbwo4FbkS1MoJsD/DsrqLzhnc4/g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/client-common": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@algolia/requester-node-http": {
|
|
||||||
"version": "5.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.52.0.tgz",
|
|
||||||
"integrity": "sha512-gyyWcLD22tnabmoit4iukCXuoRc5HYJuUjPSEa8a0D/f/NlRafpWi52AlAaa4Uu/rsl7saHsJFTNjTptWbu2+A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/client-common": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
|
|
@ -2273,21 +2012,6 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@meilisearch/instant-meilisearch": {
|
|
||||||
"version": "0.28.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@meilisearch/instant-meilisearch/-/instant-meilisearch-0.28.0.tgz",
|
|
||||||
"integrity": "sha512-QeY8w0PsoZUwzrKJyrmmi3lGRXDhzPr38t1Ns9iezoA5CD9L3YARjJJKgExybQF6KIReFw1zfJaKbPRA1xTDCA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"meilisearch": "0.53"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@meilisearch/instant-meilisearch/node_modules/meilisearch": {
|
|
||||||
"version": "0.53.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.53.0.tgz",
|
|
||||||
"integrity": "sha512-nG4VXbEOSzUmtbfsgOo+t6yX1ECEgXaT4hC0ap9MBpQGK5xwT+NWYDENYsKWR75cVaWaAqva+ok4zHlgtdXlLw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@miyaneee/rollup-plugin-json5": {
|
"node_modules/@miyaneee/rollup-plugin-json5": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@miyaneee/rollup-plugin-json5/-/rollup-plugin-json5-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@miyaneee/rollup-plugin-json5/-/rollup-plugin-json5-1.2.0.tgz",
|
||||||
|
|
@ -2432,12 +2156,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nuxt/cli/node_modules/std-env": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@nuxt/devalue": {
|
"node_modules/@nuxt/devalue": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-2.0.2.tgz",
|
||||||
|
|
@ -2664,25 +2382,25 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nuxt/icon": {
|
"node_modules/@nuxt/icon": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/icon/-/icon-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/icon/-/icon-2.2.2.tgz",
|
||||||
"integrity": "sha512-GI840yYGuvHI0BGDQ63d6rAxGzG96jQcWrnaWIQKlyQo/7sx9PjXkSHckXUXyX1MCr9zY6U25Td6OatfY6Hklw==",
|
"integrity": "sha512-K9wINW21M9x5GcKF5JEXzPKAT/Kfxl/vdnEyppw54hh5qoLcdi5HmsYoTfDP9gbJ6Z1T6IdH5JxBWk72HMe1Zg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify/collections": "^1.0.641",
|
"@iconify/collections": "^1.0.679",
|
||||||
"@iconify/types": "^2.0.0",
|
"@iconify/types": "^2.0.0",
|
||||||
"@iconify/utils": "^3.1.0",
|
"@iconify/utils": "^3.1.1",
|
||||||
"@iconify/vue": "^5.0.0",
|
"@iconify/vue": "^5.0.0",
|
||||||
"@nuxt/devtools-kit": "^3.1.1",
|
"@nuxt/devtools-kit": "^3.2.4",
|
||||||
"@nuxt/kit": "^4.2.2",
|
"@nuxt/kit": "^4.4.4",
|
||||||
"consola": "^3.4.2",
|
"consola": "^3.4.2",
|
||||||
"local-pkg": "^1.1.2",
|
"local-pkg": "^1.1.2",
|
||||||
"mlly": "^1.8.0",
|
"mlly": "^1.8.2",
|
||||||
"ohash": "^2.0.11",
|
"ohash": "^2.0.11",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"picomatch": "^4.0.3",
|
"picomatch": "^4.0.4",
|
||||||
"std-env": "^3.10.0",
|
"std-env": "^4.1.0",
|
||||||
"tinyglobby": "^0.2.15"
|
"tinyglobby": "^0.2.16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nuxt/kit": {
|
"node_modules/@nuxt/kit": {
|
||||||
|
|
@ -2780,12 +2498,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nuxt/nitro-server/node_modules/std-env": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@nuxt/schema": {
|
"node_modules/@nuxt/schema": {
|
||||||
"version": "4.4.4",
|
"version": "4.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-4.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-4.4.4.tgz",
|
||||||
|
|
@ -2802,12 +2514,6 @@
|
||||||
"node": "^14.18.0 || >=16.10.0"
|
"node": "^14.18.0 || >=16.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nuxt/schema/node_modules/std-env": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@nuxt/telemetry": {
|
"node_modules/@nuxt/telemetry": {
|
||||||
"version": "2.8.0",
|
"version": "2.8.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.8.0.tgz",
|
||||||
|
|
@ -2836,12 +2542,6 @@
|
||||||
"integrity": "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA==",
|
"integrity": "sha512-zpYTCs2byOuft65vI3z43Dd6iSdFbOZZLb9/d21aCpx2rGastVU9dOCv0lu4ykc1Ur1anAYjDi3SUvR0vq50JA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@nuxt/telemetry/node_modules/std-env": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@nuxt/ui": {
|
"node_modules/@nuxt/ui": {
|
||||||
"version": "4.7.1",
|
"version": "4.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/ui/-/ui-4.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxt/ui/-/ui-4.7.1.tgz",
|
||||||
|
|
@ -3040,12 +2740,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nuxt/vite-builder/node_modules/std-env": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@nuxtjs/color-mode": {
|
"node_modules/@nuxtjs/color-mode": {
|
||||||
"version": "3.5.2",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@nuxtjs/color-mode/-/color-mode-3.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@nuxtjs/color-mode/-/color-mode-3.5.2.tgz",
|
||||||
|
|
@ -4143,15 +3837,6 @@
|
||||||
"vue": "^3.5.0"
|
"vue": "^3.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@opentelemetry/api": {
|
|
||||||
"version": "1.9.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
|
||||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@oxc-minify/binding-android-arm-eabi": {
|
"node_modules/@oxc-minify/binding-android-arm-eabi": {
|
||||||
"version": "0.128.0",
|
"version": "0.128.0",
|
||||||
"resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm-eabi/-/binding-android-arm-eabi-0.128.0.tgz",
|
"resolved": "https://registry.npmjs.org/@oxc-minify/binding-android-arm-eabi/-/binding-android-arm-eabi-0.128.0.tgz",
|
||||||
|
|
@ -7371,12 +7056,6 @@
|
||||||
"integrity": "sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==",
|
"integrity": "sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/dom-speech-recognition": {
|
|
||||||
"version": "0.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.1.tgz",
|
|
||||||
"integrity": "sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/esrecurse": {
|
"node_modules/@types/esrecurse": {
|
||||||
"version": "4.3.1",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
||||||
|
|
@ -7395,18 +7074,6 @@
|
||||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/google.maps": {
|
|
||||||
"version": "3.64.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.64.0.tgz",
|
|
||||||
"integrity": "sha512-dN0H6tB4lgLQLovcbPXFYYOEV41TpyyJghzb5jrzjB96FZmjeOghevVdC+BMGd6YqyCqXaggyEtqRXLRjzCBZA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/hogan.js": {
|
|
||||||
"version": "3.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/hogan.js/-/hogan.js-3.0.5.tgz",
|
|
||||||
"integrity": "sha512-/uRaY3HGPWyLqOyhgvW9Aa43BNnLZrNeQxl2p8wqId4UHMfPKolSB+U7BlZyO1ng7MkLnyEAItsBzCG0SDhqrA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/json-schema": {
|
"node_modules/@types/json-schema": {
|
||||||
"version": "7.0.15",
|
"version": "7.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||||
|
|
@ -7451,12 +7118,6 @@
|
||||||
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
|
"integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/qs": {
|
|
||||||
"version": "6.15.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz",
|
|
||||||
"integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
|
|
@ -8219,15 +7880,6 @@
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vercel/oidc": {
|
|
||||||
"version": "3.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@vercel/oidc/-/oidc-3.1.0.tgz",
|
|
||||||
"integrity": "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@vitejs/plugin-vue": {
|
"node_modules/@vitejs/plugin-vue": {
|
||||||
"version": "6.0.6",
|
"version": "6.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.6.tgz",
|
||||||
|
|
@ -8724,24 +8376,6 @@
|
||||||
"node": ">= 14"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ai": {
|
|
||||||
"version": "5.0.183",
|
|
||||||
"resolved": "https://registry.npmjs.org/ai/-/ai-5.0.183.tgz",
|
|
||||||
"integrity": "sha512-lmLFkxJ2epeUXi6QXi/9VYs1HF61vikpP8vGnGd3Erdh/syUyfZ/DC1to2AoNwytBNpICN3OGbTpwc7jfPewgg==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@ai-sdk/gateway": "2.0.86",
|
|
||||||
"@ai-sdk/provider": "2.0.3",
|
|
||||||
"@ai-sdk/provider-utils": "3.0.25",
|
|
||||||
"@opentelemetry/api": "1.9.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"zod": "^3.25.76 || ^4.1.8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.15.0",
|
"version": "6.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
||||||
|
|
@ -8758,44 +8392,6 @@
|
||||||
"url": "https://github.com/sponsors/epoberezkin"
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/algoliasearch": {
|
|
||||||
"version": "5.52.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.0.tgz",
|
|
||||||
"integrity": "sha512-0ZzY9mjqV7gop/AH8pIBiAS8giXP7WcSiUfoFYIzYAK9QC5c37E4SIVtJVBMwlURc0/uNt2o4RcNRvdHa4CJ5w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/abtesting": "1.18.0",
|
|
||||||
"@algolia/client-abtesting": "5.52.0",
|
|
||||||
"@algolia/client-analytics": "5.52.0",
|
|
||||||
"@algolia/client-common": "5.52.0",
|
|
||||||
"@algolia/client-insights": "5.52.0",
|
|
||||||
"@algolia/client-personalization": "5.52.0",
|
|
||||||
"@algolia/client-query-suggestions": "5.52.0",
|
|
||||||
"@algolia/client-search": "5.52.0",
|
|
||||||
"@algolia/ingestion": "1.52.0",
|
|
||||||
"@algolia/monitoring": "1.52.0",
|
|
||||||
"@algolia/recommend": "5.52.0",
|
|
||||||
"@algolia/requester-browser-xhr": "5.52.0",
|
|
||||||
"@algolia/requester-fetch": "5.52.0",
|
|
||||||
"@algolia/requester-node-http": "5.52.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/algoliasearch-helper": {
|
|
||||||
"version": "3.26.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.26.0.tgz",
|
|
||||||
"integrity": "sha512-Rv2x3GXleQ3ygwhkhJubhhYGsICmShLAiqtUuJTUkr9uOCOXyF2E71LVT4XDnVffbknv8XgScP4U0Oxtgm+hIw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/events": "^4.0.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"algoliasearch": ">= 3.1 < 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/alien-signals": {
|
"node_modules/alien-signals": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.1.2.tgz",
|
||||||
|
|
@ -10820,6 +10416,12 @@
|
||||||
"url": "https://dotenvx.com"
|
"url": "https://dotenvx.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/driver.js": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
|
@ -11795,15 +11397,6 @@
|
||||||
"bare-events": "^2.7.0"
|
"bare-events": "^2.7.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eventsource-parser": {
|
|
||||||
"version": "3.0.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz",
|
|
||||||
"integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/execa": {
|
"node_modules/execa": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
|
||||||
|
|
@ -12620,51 +12213,12 @@
|
||||||
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==",
|
"integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/hogan.js": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==",
|
|
||||||
"dependencies": {
|
|
||||||
"mkdirp": "0.3.0",
|
|
||||||
"nopt": "1.0.10"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"hulk": "bin/hulk"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hogan.js/node_modules/abbrev": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/hogan.js/node_modules/nopt": {
|
|
||||||
"version": "1.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz",
|
|
||||||
"integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"abbrev": "1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"nopt": "bin/nopt.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hookable": {
|
"node_modules/hookable": {
|
||||||
"version": "6.1.1",
|
"version": "6.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz",
|
||||||
"integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==",
|
"integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/htm": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/html-entities": {
|
"node_modules/html-entities": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
|
||||||
|
|
@ -12850,88 +12404,6 @@
|
||||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/instantsearch-ui-components": {
|
|
||||||
"version": "0.12.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/instantsearch-ui-components/-/instantsearch-ui-components-0.12.0.tgz",
|
|
||||||
"integrity": "sha512-WcyrNJ5sqeqAWLibztnssSKDhgFEdj4C1prZOHSCONxF+5RSfCsD0oqKiZrTMx3dlAL/Qx7eZxKhD56u5mxJMA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@babel/runtime": "^7.27.6",
|
|
||||||
"ai": "^5.0.18",
|
|
||||||
"markdown-to-jsx": "^7.7.15",
|
|
||||||
"zod": "^3.25.76 || ^4",
|
|
||||||
"zod-to-json-schema": "3.24.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/instantsearch-ui-components/node_modules/zod": {
|
|
||||||
"version": "3.25.76",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/instantsearch-ui-components/node_modules/zod-to-json-schema": {
|
|
||||||
"version": "3.24.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
|
|
||||||
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
|
|
||||||
"license": "ISC",
|
|
||||||
"peerDependencies": {
|
|
||||||
"zod": "^3.24.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/instantsearch.css": {
|
|
||||||
"version": "8.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/instantsearch.css/-/instantsearch.css-8.6.0.tgz",
|
|
||||||
"integrity": "sha512-YweaPxsvaITBgKPxEZnuTX0XLfa4JeLBGkhrlcN2glCH1Ipk9XU1M9X+clcAEottTe8K1/q5COePW0Ozr2+0MA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/instantsearch.js": {
|
|
||||||
"version": "4.81.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/instantsearch.js/-/instantsearch.js-4.81.0.tgz",
|
|
||||||
"integrity": "sha512-zyIUSrikpst5eaBMrruGRGmRHOHQciDyJ8UW7i4xhPVNRwxr5Ls/NBez9AdNne+7OvALCOeaTI3t9LoBQakWhw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@algolia/events": "^4.0.1",
|
|
||||||
"@types/dom-speech-recognition": "^0.0.1",
|
|
||||||
"@types/google.maps": "^3.55.12",
|
|
||||||
"@types/hogan.js": "^3.0.0",
|
|
||||||
"@types/qs": "^6.5.3",
|
|
||||||
"ai": "^5.0.18",
|
|
||||||
"algoliasearch-helper": "3.26.0",
|
|
||||||
"hogan.js": "^3.0.2",
|
|
||||||
"htm": "^3.0.0",
|
|
||||||
"instantsearch-ui-components": "0.12.0",
|
|
||||||
"preact": "^10.10.0",
|
|
||||||
"qs": "^6.5.1 < 6.10",
|
|
||||||
"react": ">= 0.14.0",
|
|
||||||
"search-insights": "^2.17.2",
|
|
||||||
"zod": "^3.25.76 || ^4",
|
|
||||||
"zod-to-json-schema": "3.24.6"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"algoliasearch": ">= 3.1 < 6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/instantsearch.js/node_modules/zod": {
|
|
||||||
"version": "3.25.76",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/instantsearch.js/node_modules/zod-to-json-schema": {
|
|
||||||
"version": "3.24.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz",
|
|
||||||
"integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==",
|
|
||||||
"license": "ISC",
|
|
||||||
"peerDependencies": {
|
|
||||||
"zod": "^3.24.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/internmap": {
|
"node_modules/internmap": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
|
@ -13276,12 +12748,6 @@
|
||||||
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/json-schema": {
|
|
||||||
"version": "0.4.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
|
|
||||||
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==",
|
|
||||||
"license": "(AFL-2.1 OR BSD-3-Clause)"
|
|
||||||
},
|
|
||||||
"node_modules/json-schema-to-typescript-lite": {
|
"node_modules/json-schema-to-typescript-lite": {
|
||||||
"version": "15.0.0",
|
"version": "15.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/json-schema-to-typescript-lite/-/json-schema-to-typescript-lite-15.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/json-schema-to-typescript-lite/-/json-schema-to-typescript-lite-15.0.0.tgz",
|
||||||
|
|
@ -13808,12 +13274,6 @@
|
||||||
"listhen": "bin/listhen.mjs"
|
"listhen": "bin/listhen.mjs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/listhen/node_modules/std-env": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/load-tsconfig": {
|
"node_modules/load-tsconfig": {
|
||||||
"version": "0.2.5",
|
"version": "0.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz",
|
||||||
|
|
@ -14022,23 +13482,6 @@
|
||||||
"vt-pbf": "^3.1.3"
|
"vt-pbf": "^3.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/markdown-to-jsx": {
|
|
||||||
"version": "7.7.17",
|
|
||||||
"resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.7.17.tgz",
|
|
||||||
"integrity": "sha512-7mG/1feQ0TX5I7YyMZVDgCC/y2I3CiEhIRQIhyov9nGBP5eoVrOXXHuL5ZP8GRfxVZKRiXWJgwXkb9It+nQZfQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": ">= 0.14.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"react": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/marked": {
|
"node_modules/marked": {
|
||||||
"version": "17.0.6",
|
"version": "17.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.6.tgz",
|
||||||
|
|
@ -14066,12 +13509,6 @@
|
||||||
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
"node_modules/meilisearch": {
|
|
||||||
"version": "0.54.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/meilisearch/-/meilisearch-0.54.0.tgz",
|
|
||||||
"integrity": "sha512-b1bwJAEfj8C6hgSN88+/LvW3pe3nWC+thBS2seAbPZGakf/vzsLqppgZquiomzCr2GhU7U7H289625qhYe3rbw==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/merge-stream": {
|
"node_modules/merge-stream": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||||
|
|
@ -14209,22 +13646,6 @@
|
||||||
"node": ">= 18"
|
"node": ">= 18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mitt": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/mkdirp": {
|
|
||||||
"version": "0.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz",
|
|
||||||
"integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==",
|
|
||||||
"deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)",
|
|
||||||
"license": "MIT/X11",
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mlly": {
|
"node_modules/mlly": {
|
||||||
"version": "1.8.2",
|
"version": "1.8.2",
|
||||||
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz",
|
||||||
|
|
@ -14956,12 +14377,6 @@
|
||||||
"node": ">= 12"
|
"node": ">= 12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nitropack/node_modules/std-env": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/nitropack/node_modules/unimport": {
|
"node_modules/nitropack/node_modules/unimport": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/unimport/-/unimport-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/unimport/-/unimport-6.2.0.tgz",
|
||||||
|
|
@ -15204,58 +14619,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nuxt-meilisearch": {
|
"node_modules/nuxt-driver.js": {
|
||||||
"version": "1.4.17",
|
"version": "0.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/nuxt-meilisearch/-/nuxt-meilisearch-1.4.17.tgz",
|
"resolved": "https://registry.npmjs.org/nuxt-driver.js/-/nuxt-driver.js-0.1.1.tgz",
|
||||||
"integrity": "sha512-9O0dUIwuu00YAGVqQ0BVp/7kgLf3a6ONeOy6gDYtgj8sw/VI1o5tYIwg9hMjesOD1Jw/2WGL8dORkNRN2Mwedg==",
|
"integrity": "sha512-K8SJBzLD8oyu3mhp2vv6F6/SUEaXSp067iGLCPDiMGh4w86VdDArapPmyylH9ZvYq/aeltVPbS6S/uOuVtAXHw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@meilisearch/instant-meilisearch": "0.28.0",
|
"@nuxt/kit": "^4.4.4",
|
||||||
"@nuxt/kit": "4.2.0",
|
"driver.js": "^1.4.0"
|
||||||
"instantsearch.css": "8.6.0",
|
|
||||||
"meilisearch": "0.54.0",
|
|
||||||
"vue-instantsearch": "4.22.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nuxt-meilisearch/node_modules/@nuxt/kit": {
|
|
||||||
"version": "4.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-4.2.0.tgz",
|
|
||||||
"integrity": "sha512-1yN3LL6RDN5GjkNLPUYCbNRkaYnat6hqejPyfIBBVzrWOrpiQeNMGxQM/IcVdaSuBJXAnu0sUvTKXpXkmPhljg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"c12": "^3.3.1",
|
|
||||||
"consola": "^3.4.2",
|
|
||||||
"defu": "^6.1.4",
|
|
||||||
"destr": "^2.0.5",
|
|
||||||
"errx": "^0.1.0",
|
|
||||||
"exsolve": "^1.0.7",
|
|
||||||
"ignore": "^7.0.5",
|
|
||||||
"jiti": "^2.6.1",
|
|
||||||
"klona": "^2.0.6",
|
|
||||||
"mlly": "^1.8.0",
|
|
||||||
"ohash": "^2.0.11",
|
|
||||||
"pathe": "^2.0.3",
|
|
||||||
"pkg-types": "^2.3.0",
|
|
||||||
"rc9": "^2.1.2",
|
|
||||||
"scule": "^1.3.0",
|
|
||||||
"semver": "^7.7.3",
|
|
||||||
"tinyglobby": "^0.2.15",
|
|
||||||
"ufo": "^1.6.1",
|
|
||||||
"unctx": "^2.4.1",
|
|
||||||
"untyped": "^2.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nuxt-meilisearch/node_modules/rc9": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"defu": "^6.1.4",
|
|
||||||
"destr": "^2.0.3"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nuxt/node_modules/cookie-es": {
|
"node_modules/nuxt/node_modules/cookie-es": {
|
||||||
|
|
@ -15285,12 +14656,6 @@
|
||||||
"@types/estree": "^1.0.0"
|
"@types/estree": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/nuxt/node_modules/std-env": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/nuxt/node_modules/unimport": {
|
"node_modules/nuxt/node_modules/unimport": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/unimport/-/unimport-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/unimport/-/unimport-6.2.0.tgz",
|
||||||
|
|
@ -16362,16 +15727,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/preact": {
|
|
||||||
"version": "10.29.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/preact/-/preact-10.29.1.tgz",
|
|
||||||
"integrity": "sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/preact"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
|
@ -16578,18 +15933,6 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/qs": {
|
|
||||||
"version": "6.9.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.9.9.tgz",
|
|
||||||
"integrity": "sha512-4mp+ySf3n7scxSqU6LIO9jjYCJWIdbXh3EQ8uvEc1uXiZm8sLI7moRTOvZb66c8zEHelBGEVRkSW6e9JCaayTw==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/quansync": {
|
"node_modules/quansync": {
|
||||||
"version": "0.2.11",
|
"version": "0.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz",
|
||||||
|
|
@ -16657,15 +16000,6 @@
|
||||||
"destr": "^2.0.5"
|
"destr": "^2.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react": {
|
|
||||||
"version": "19.2.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
|
||||||
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/readable-stream": {
|
"node_modules/readable-stream": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
|
||||||
|
|
@ -17111,12 +16445,6 @@
|
||||||
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
"integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/search-insights": {
|
|
||||||
"version": "2.17.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz",
|
|
||||||
"integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "7.7.4",
|
"version": "7.7.4",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||||
|
|
@ -17419,9 +16747,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/std-env": {
|
"node_modules/std-env": {
|
||||||
"version": "3.10.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz",
|
||||||
"integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
|
"integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/streamx": {
|
"node_modules/streamx": {
|
||||||
|
|
@ -19187,31 +18515,6 @@
|
||||||
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/vue-instantsearch": {
|
|
||||||
"version": "4.22.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-instantsearch/-/vue-instantsearch-4.22.0.tgz",
|
|
||||||
"integrity": "sha512-/BUdzD+NAybC81FGzEeSuCSYobYUVgHEHI7QY0HlTzdxIHOqWlw2y152/80kGKndS0C+Yu5ifGGg+xVIcFc4Dg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"instantsearch-ui-components": "0.12.0",
|
|
||||||
"instantsearch.js": "4.81.0",
|
|
||||||
"mitt": "^2.1.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@vue/server-renderer": "^3.1.2",
|
|
||||||
"algoliasearch": ">= 3.32.0 < 6",
|
|
||||||
"vue": "^2.6.0 || >=3.0.0-rc.0",
|
|
||||||
"vue-server-renderer": "^2.6.11"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@vue/server-renderer": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"vue-server-renderer": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-router": {
|
"node_modules/vue-router": {
|
||||||
"version": "5.0.6",
|
"version": "5.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-5.0.6.tgz",
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.20",
|
"dayjs": "^1.11.20",
|
||||||
"nuxt": "^4.4.2",
|
"nuxt": "^4.4.2",
|
||||||
"nuxt-meilisearch": "^1.4.17",
|
"nuxt-driver.js": "^0.1.1",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"scule": "^1.3.0",
|
"scule": "^1.3.0",
|
||||||
"tailwindcss": "^4.2.4",
|
"tailwindcss": "^4.2.4",
|
||||||
|
|
@ -42,5 +42,5 @@
|
||||||
"typescript": "^6.0.3",
|
"typescript": "^6.0.3",
|
||||||
"vue-tsc": "^3.2.7"
|
"vue-tsc": "^3.2.7"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.33.2"
|
"packageManager": "pnpm@11.5.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
506
pnpm-lock.yaml
506
pnpm-lock.yaml
|
|
@ -22,7 +22,7 @@ importers:
|
||||||
version: 3.12.1
|
version: 3.12.1
|
||||||
'@nuxt/ui':
|
'@nuxt/ui':
|
||||||
specifier: ^4.7.0
|
specifier: ^4.7.0
|
||||||
version: 4.7.1(@internationalized/date@3.12.1)(@internationalized/number@3.6.6)(@tiptap/extensions@3.23.1(@tiptap/core@3.23.1(@tiptap/pm@3.23.1))(@tiptap/pm@3.23.1))(@tiptap/y-tiptap@3.0.3(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30))(axios@1.16.0)(change-case@5.4.4)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.10.1)(magicast@0.5.2)(react@19.2.6)(tailwindcss@4.3.0)(typescript@6.0.3)(vite@7.3.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0))(vue-router@4.6.4(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))(yjs@13.6.30)(zod@4.4.3)
|
version: 4.7.1(@internationalized/date@3.12.1)(@internationalized/number@3.6.6)(@tiptap/extensions@3.23.1(@tiptap/core@3.23.1(@tiptap/pm@3.23.1))(@tiptap/pm@3.23.1))(@tiptap/y-tiptap@3.0.3(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30))(axios@1.16.0)(change-case@5.4.4)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.10.1)(magicast@0.5.2)(tailwindcss@4.3.0)(typescript@6.0.3)(vite@7.3.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0))(vue-router@4.6.4(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))(yjs@13.6.30)(zod@4.4.3)
|
||||||
'@nuxtjs/i18n':
|
'@nuxtjs/i18n':
|
||||||
specifier: ^9.5.6
|
specifier: ^9.5.6
|
||||||
version: 9.5.6(@vue/compiler-dom@3.5.34)(eslint@10.3.0(jiti@2.7.0))(magicast@0.5.2)(rollup@4.60.3)(vue@3.5.34(typescript@6.0.3))
|
version: 9.5.6(@vue/compiler-dom@3.5.34)(eslint@10.3.0(jiti@2.7.0))(magicast@0.5.2)(rollup@4.60.3)(vue@3.5.34(typescript@6.0.3))
|
||||||
|
|
@ -56,9 +56,6 @@ importers:
|
||||||
nuxt:
|
nuxt:
|
||||||
specifier: ^4.4.2
|
specifier: ^4.4.2
|
||||||
version: 4.4.5(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@vue/compiler-sfc@3.5.34)(cac@6.7.14)(db0@0.3.4)(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.2)(optionator@0.9.4)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(rollup-plugin-visualizer@7.0.1(rollup@4.60.3))(rollup@4.60.3)(srvx@0.11.15)(terser@5.47.1)(typescript@6.0.3)(vite@7.3.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.8(typescript@6.0.3))(yaml@2.9.0)
|
version: 4.4.5(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@vue/compiler-sfc@3.5.34)(cac@6.7.14)(db0@0.3.4)(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.2)(optionator@0.9.4)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(rollup-plugin-visualizer@7.0.1(rollup@4.60.3))(rollup@4.60.3)(srvx@0.11.15)(terser@5.47.1)(typescript@6.0.3)(vite@7.3.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.8(typescript@6.0.3))(yaml@2.9.0)
|
||||||
nuxt-meilisearch:
|
|
||||||
specifier: ^1.4.17
|
|
||||||
version: 1.4.17(@vue/server-renderer@3.5.34(vue@3.5.34(typescript@6.0.3)))(algoliasearch@5.52.1)(magicast@0.5.2)(react@19.2.6)(vue@3.5.34(typescript@6.0.3))
|
|
||||||
pinia:
|
pinia:
|
||||||
specifier: ^3.0.4
|
specifier: ^3.0.4
|
||||||
version: 3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
version: 3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3))
|
||||||
|
|
@ -96,81 +93,6 @@ importers:
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@ai-sdk/gateway@2.0.88':
|
|
||||||
resolution: {integrity: sha512-H62l0gxr4K0rdR2WHbvck2wOKMsocAjdZg41Exsj9Qf5/TyAuHzcNt9jKNv5t2vRFXFZaCpbC5uCCxgUC/GiaA==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
peerDependencies:
|
|
||||||
zod: ^3.25.76 || ^4.1.8
|
|
||||||
|
|
||||||
'@ai-sdk/provider-utils@3.0.25':
|
|
||||||
resolution: {integrity: sha512-CvsRu+32Y8a167s+lrIBtsybvgTHp8j9y+6BeTvLeoW3Q+okw/b4CnNUFOLIXsRaKHQKAH+IHNJPYWywfpw0LA==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
peerDependencies:
|
|
||||||
zod: ^3.25.76 || ^4.1.8
|
|
||||||
|
|
||||||
'@ai-sdk/provider@2.0.3':
|
|
||||||
resolution: {integrity: sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
|
|
||||||
'@algolia/abtesting@1.18.1':
|
|
||||||
resolution: {integrity: sha512-aehCadlWOGvrT91KUIZpC0MbB8KBW9yUuvTJFd2xesR7le/IsT4nJUnjCCZ4ZqZCeTcPHPV5mo//fZ5oxcSVYw==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@algolia/client-abtesting@5.52.1':
|
|
||||||
resolution: {integrity: sha512-HmXOGBOAOJPounpBzBpuY0zDYeiCpxgHnQmuA7JO6ScukcBdGp3/XM9zJk5pJx/xNGD68mbPGXWpDxGtl6BwDQ==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@algolia/client-analytics@5.52.1':
|
|
||||||
resolution: {integrity: sha512-5oo4+I8iixie9vXhCyNFCzeIr8pqA3FQ//VsLHTDvZAV4ttYOPGvYHGQq5NSalrLx5Jc3dRro/5uDOlnUMcBJg==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@algolia/client-common@5.52.1':
|
|
||||||
resolution: {integrity: sha512-qCDoZfx5MpX7XQzvQ3bC4tSEMkQWQMaF/ABtLuoze03Y/flR563CCSws02qIJ23oX7lxl92LsilZjINVyTdtLw==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@algolia/client-insights@5.52.1':
|
|
||||||
resolution: {integrity: sha512-hnGs0/lsFJ2PWDxNBz7pxreXo/Xz7gxYRcfePBUjsH26ad0kU/sgnVZd9LwWBpsQv65z2jlb5dkyaB9WE9M9FQ==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@algolia/client-personalization@5.52.1':
|
|
||||||
resolution: {integrity: sha512-2VxxNc/uBysyKvGeBdSM5n9eIDKH8kWD7wd9/yqbJAiVwU4Yv6tU1LSJusHKrXV/aCu1KW7t9Gug9QyeEmtn/Q==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@algolia/client-query-suggestions@5.52.1':
|
|
||||||
resolution: {integrity: sha512-O6mPtsw3xEfNOe6gWFpYLeAZAIljNa4Hgna3bq15PwyN7nbjTY0wXJFRbzs/0YVf75Br+SbOQUmjKxXYjDiSiQ==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@algolia/client-search@5.52.1':
|
|
||||||
resolution: {integrity: sha512-gA8oJOV1LnQQkDf91iebNnFInHuW0gRPEgLSOQ7EfipCEjYTHm5swm1DlH9H5RaRw4RrHuzHBegnlzc0MAstcg==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@algolia/events@4.0.1':
|
|
||||||
resolution: {integrity: sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==}
|
|
||||||
|
|
||||||
'@algolia/ingestion@1.52.1':
|
|
||||||
resolution: {integrity: sha512-U9zZfc5xIu9wRxZkt+HceJUAD4VKHKbAyLSloJdEyMRmphXeibfrY9cxqIXBcmPeZzGhn3Imb35Dq8l19PkJhw==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@algolia/monitoring@1.52.1':
|
|
||||||
resolution: {integrity: sha512-a3SGNceHmkQfq77iG8Ka+w1pvwfZa/0lzEIgse30fL0kD+yKnd/dg0dQvSfFPAEt2f21DMcGkDSSeJlO3KdQjQ==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@algolia/recommend@5.52.1':
|
|
||||||
resolution: {integrity: sha512-z98QEguCFDpxb4S/PyrUK1igqF8tPsdbqOUUO6ON91vJ58w+Gwa6ncrI0oNXSFcrkxA5EqPKPQ2A1PBCn08TYQ==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@algolia/requester-browser-xhr@5.52.1':
|
|
||||||
resolution: {integrity: sha512-CI7+/0I11QeZM59Uc8whd2or0kqzFVjpaPn9Qpwll/krHcBAxk24WkAQ6WX+IwDVMfpont4YGbKwAmCre3vE8Q==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@algolia/requester-fetch@5.52.1':
|
|
||||||
resolution: {integrity: sha512-S6bDuw9byfOvm3T71cgdoZgrgnZq6hpdMLkx52Louh57nUAmvGQESz2aojOynQHjbTiV55smvAFbgn0qT4tJrg==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@algolia/requester-node-http@5.52.1':
|
|
||||||
resolution: {integrity: sha512-tqZXM+54rWo4mk5jL5Z/flE11nPmNEdXwFBM5py9DkOmbjeCNemfVd45FyM97XdzfZ0dl9uOJC6PYn1FpkeyQg==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0':
|
'@alloc/quick-lru@5.2.0':
|
||||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -1121,9 +1043,6 @@ packages:
|
||||||
resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==}
|
resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
'@meilisearch/instant-meilisearch@0.28.0':
|
|
||||||
resolution: {integrity: sha512-QeY8w0PsoZUwzrKJyrmmi3lGRXDhzPr38t1Ns9iezoA5CD9L3YARjJJKgExybQF6KIReFw1zfJaKbPRA1xTDCA==}
|
|
||||||
|
|
||||||
'@miyaneee/rollup-plugin-json5@1.2.0':
|
'@miyaneee/rollup-plugin-json5@1.2.0':
|
||||||
resolution: {integrity: sha512-JjTIaXZp9WzhUHpElrqPnl1AzBi/rvRs065F71+aTmlqvTMVkdbjZ8vfFl4nRlgJy+TPBw69ZK4pwFdmOAt4aA==}
|
resolution: {integrity: sha512-JjTIaXZp9WzhUHpElrqPnl1AzBi/rvRs065F71+aTmlqvTMVkdbjZ8vfFl4nRlgJy+TPBw69ZK4pwFdmOAt4aA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -1218,10 +1137,6 @@ packages:
|
||||||
resolution: {integrity: sha512-eGo9DjJ9NzKMbJpFU/UTd4c5iOSYuivghKD8W/jVGHs7kew+hdSMvUy401IfQB7EObKPvt/WXEutAIaTg9OsyA==}
|
resolution: {integrity: sha512-eGo9DjJ9NzKMbJpFU/UTd4c5iOSYuivghKD8W/jVGHs7kew+hdSMvUy401IfQB7EObKPvt/WXEutAIaTg9OsyA==}
|
||||||
engines: {node: '>=18.12.0'}
|
engines: {node: '>=18.12.0'}
|
||||||
|
|
||||||
'@nuxt/kit@4.2.0':
|
|
||||||
resolution: {integrity: sha512-1yN3LL6RDN5GjkNLPUYCbNRkaYnat6hqejPyfIBBVzrWOrpiQeNMGxQM/IcVdaSuBJXAnu0sUvTKXpXkmPhljg==}
|
|
||||||
engines: {node: '>=18.12.0'}
|
|
||||||
|
|
||||||
'@nuxt/kit@4.4.5':
|
'@nuxt/kit@4.4.5':
|
||||||
resolution: {integrity: sha512-J0BpoOomzd3iVZozYlZJ7AwAVliXRgeChZnAkQLfg8d0h/Q+aMK9kkHuhwFULASaRn5idiD4BIhOUz7/uoLbSw==}
|
resolution: {integrity: sha512-J0BpoOomzd3iVZozYlZJ7AwAVliXRgeChZnAkQLfg8d0h/Q+aMK9kkHuhwFULASaRn5idiD4BIhOUz7/uoLbSw==}
|
||||||
engines: {node: '>=18.12.0'}
|
engines: {node: '>=18.12.0'}
|
||||||
|
|
@ -1316,10 +1231,6 @@ packages:
|
||||||
resolution: {integrity: sha512-PhrQtJT6Di9uoslL5BTrBFqntFlfCaUKlO3T9ORJwmWFdowPqQeFjQ9OjVbKA6TNWr3kQhDqLbIcGlhbuG1USQ==}
|
resolution: {integrity: sha512-PhrQtJT6Di9uoslL5BTrBFqntFlfCaUKlO3T9ORJwmWFdowPqQeFjQ9OjVbKA6TNWr3kQhDqLbIcGlhbuG1USQ==}
|
||||||
engines: {node: '>=18.12.0'}
|
engines: {node: '>=18.12.0'}
|
||||||
|
|
||||||
'@opentelemetry/api@1.9.0':
|
|
||||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
|
||||||
engines: {node: '>=8.0.0'}
|
|
||||||
|
|
||||||
'@oxc-minify/binding-android-arm-eabi@0.128.0':
|
'@oxc-minify/binding-android-arm-eabi@0.128.0':
|
||||||
resolution: {integrity: sha512-EwdDhZLRmXxSnfy0v9gdOru7TutM8ItRg1Xv8e2B4boWMnHlFCIH38JfwgQnenbkF8SVTwVJtDCkmwEzN4q3xA==}
|
resolution: {integrity: sha512-EwdDhZLRmXxSnfy0v9gdOru7TutM8ItRg1Xv8e2B4boWMnHlFCIH38JfwgQnenbkF8SVTwVJtDCkmwEzN4q3xA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
|
@ -2625,9 +2536,6 @@ packages:
|
||||||
'@types/dagre@0.7.54':
|
'@types/dagre@0.7.54':
|
||||||
resolution: {integrity: sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==}
|
resolution: {integrity: sha512-QjcRY+adGbYvBFS7cwv5txhVIwX1XXIUswWl+kSQTbI6NjgZydrZkEKX/etzVd7i+bCsCb40Z/xlBY5eoFuvWQ==}
|
||||||
|
|
||||||
'@types/dom-speech-recognition@0.0.1':
|
|
||||||
resolution: {integrity: sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw==}
|
|
||||||
|
|
||||||
'@types/esrecurse@4.3.1':
|
'@types/esrecurse@4.3.1':
|
||||||
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
|
resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==}
|
||||||
|
|
||||||
|
|
@ -2640,12 +2548,6 @@ packages:
|
||||||
'@types/geojson@7946.0.16':
|
'@types/geojson@7946.0.16':
|
||||||
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
|
||||||
|
|
||||||
'@types/google.maps@3.64.0':
|
|
||||||
resolution: {integrity: sha512-dN0H6tB4lgLQLovcbPXFYYOEV41TpyyJghzb5jrzjB96FZmjeOghevVdC+BMGd6YqyCqXaggyEtqRXLRjzCBZA==}
|
|
||||||
|
|
||||||
'@types/hogan.js@3.0.5':
|
|
||||||
resolution: {integrity: sha512-/uRaY3HGPWyLqOyhgvW9Aa43BNnLZrNeQxl2p8wqId4UHMfPKolSB+U7BlZyO1ng7MkLnyEAItsBzCG0SDhqrA==}
|
|
||||||
|
|
||||||
'@types/json-schema@7.0.15':
|
'@types/json-schema@7.0.15':
|
||||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||||
|
|
||||||
|
|
@ -2664,9 +2566,6 @@ packages:
|
||||||
'@types/pbf@3.0.5':
|
'@types/pbf@3.0.5':
|
||||||
resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==}
|
resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==}
|
||||||
|
|
||||||
'@types/qs@6.15.1':
|
|
||||||
resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==}
|
|
||||||
|
|
||||||
'@types/resolve@1.20.2':
|
'@types/resolve@1.20.2':
|
||||||
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
|
||||||
|
|
||||||
|
|
@ -2887,10 +2786,6 @@ packages:
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@vercel/oidc@3.1.0':
|
|
||||||
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
|
|
||||||
engines: {node: '>= 20'}
|
|
||||||
|
|
||||||
'@vitejs/plugin-vue-jsx@5.1.5':
|
'@vitejs/plugin-vue-jsx@5.1.5':
|
||||||
resolution: {integrity: sha512-jIAsvHOEtWpslLOI2MeElGFxH7M8pM83BU/Tor4RLyiwH0FM4nUW3xdvbw20EeU9wc5IspQwMq225K3CMnJEpA==}
|
resolution: {integrity: sha512-jIAsvHOEtWpslLOI2MeElGFxH7M8pM83BU/Tor4RLyiwH0FM4nUW3xdvbw20EeU9wc5IspQwMq225K3CMnJEpA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
|
@ -3076,9 +2971,6 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.5.0
|
vue: ^3.5.0
|
||||||
|
|
||||||
abbrev@1.1.1:
|
|
||||||
resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==}
|
|
||||||
|
|
||||||
abbrev@3.0.1:
|
abbrev@3.0.1:
|
||||||
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
|
resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
|
|
@ -3106,24 +2998,9 @@ packages:
|
||||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
ai@5.0.186:
|
|
||||||
resolution: {integrity: sha512-0HVwYO9k/x5eSNggqya/75uirBLjkZoL5QdNp9ftjOCl/IXWSzqys/SzsL3ifWBz603a0KbW+EZyYVtmbFJrTQ==}
|
|
||||||
engines: {node: '>=18'}
|
|
||||||
peerDependencies:
|
|
||||||
zod: ^3.25.76 || ^4.1.8
|
|
||||||
|
|
||||||
ajv@6.15.0:
|
ajv@6.15.0:
|
||||||
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
|
resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==}
|
||||||
|
|
||||||
algoliasearch-helper@3.26.0:
|
|
||||||
resolution: {integrity: sha512-Rv2x3GXleQ3ygwhkhJubhhYGsICmShLAiqtUuJTUkr9uOCOXyF2E71LVT4XDnVffbknv8XgScP4U0Oxtgm+hIw==}
|
|
||||||
peerDependencies:
|
|
||||||
algoliasearch: '>= 3.1 < 6'
|
|
||||||
|
|
||||||
algoliasearch@5.52.1:
|
|
||||||
resolution: {integrity: sha512-fHA8+kXTbjagw3jkLiaS7KKrH8qe2DyOsiUhGlN4cdT77PEsfqXZl7ewDk1hsg+pJnPlnE50XtLxjR91iJOpmg==}
|
|
||||||
engines: {node: '>= 14.0.0'}
|
|
||||||
|
|
||||||
alien-signals@3.1.2:
|
alien-signals@3.1.2:
|
||||||
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
|
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
|
||||||
|
|
||||||
|
|
@ -4134,10 +4011,6 @@ packages:
|
||||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||||
engines: {node: '>=0.8.x'}
|
engines: {node: '>=0.8.x'}
|
||||||
|
|
||||||
eventsource-parser@3.0.8:
|
|
||||||
resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==}
|
|
||||||
engines: {node: '>=18.0.0'}
|
|
||||||
|
|
||||||
execa@8.0.1:
|
execa@8.0.1:
|
||||||
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
||||||
engines: {node: '>=16.17'}
|
engines: {node: '>=16.17'}
|
||||||
|
|
@ -4403,19 +4276,12 @@ packages:
|
||||||
hey-listen@1.0.8:
|
hey-listen@1.0.8:
|
||||||
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
|
resolution: {integrity: sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==}
|
||||||
|
|
||||||
hogan.js@3.0.2:
|
|
||||||
resolution: {integrity: sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
hookable@5.5.3:
|
hookable@5.5.3:
|
||||||
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==}
|
||||||
|
|
||||||
hookable@6.1.1:
|
hookable@6.1.1:
|
||||||
resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==}
|
resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==}
|
||||||
|
|
||||||
htm@3.1.1:
|
|
||||||
resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==}
|
|
||||||
|
|
||||||
html-entities@2.6.0:
|
html-entities@2.6.0:
|
||||||
resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
|
resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==}
|
||||||
|
|
||||||
|
|
@ -4484,17 +4350,6 @@ packages:
|
||||||
resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
|
resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==}
|
||||||
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
instantsearch-ui-components@0.12.0:
|
|
||||||
resolution: {integrity: sha512-WcyrNJ5sqeqAWLibztnssSKDhgFEdj4C1prZOHSCONxF+5RSfCsD0oqKiZrTMx3dlAL/Qx7eZxKhD56u5mxJMA==}
|
|
||||||
|
|
||||||
instantsearch.css@8.6.0:
|
|
||||||
resolution: {integrity: sha512-YweaPxsvaITBgKPxEZnuTX0XLfa4JeLBGkhrlcN2glCH1Ipk9XU1M9X+clcAEottTe8K1/q5COePW0Ozr2+0MA==}
|
|
||||||
|
|
||||||
instantsearch.js@4.81.0:
|
|
||||||
resolution: {integrity: sha512-zyIUSrikpst5eaBMrruGRGmRHOHQciDyJ8UW7i4xhPVNRwxr5Ls/NBez9AdNne+7OvALCOeaTI3t9LoBQakWhw==}
|
|
||||||
peerDependencies:
|
|
||||||
algoliasearch: '>= 3.1 < 6'
|
|
||||||
|
|
||||||
internmap@1.0.1:
|
internmap@1.0.1:
|
||||||
resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==}
|
resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==}
|
||||||
|
|
||||||
|
|
@ -4631,9 +4486,6 @@ packages:
|
||||||
json-schema-traverse@0.4.1:
|
json-schema-traverse@0.4.1:
|
||||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||||
|
|
||||||
json-schema@0.4.0:
|
|
||||||
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
|
|
||||||
|
|
||||||
json-stable-stringify-without-jsonify@1.0.1:
|
json-stable-stringify-without-jsonify@1.0.1:
|
||||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||||
|
|
||||||
|
|
@ -4842,15 +4694,6 @@ packages:
|
||||||
maplibre-gl@2.4.0:
|
maplibre-gl@2.4.0:
|
||||||
resolution: {integrity: sha512-csNFylzntPmHWidczfgCZpvbTSmhaWvLRj9e1ezUDBEPizGgshgm3ea1T5TCNEEBq0roauu7BPuRZjA3wO4KqA==}
|
resolution: {integrity: sha512-csNFylzntPmHWidczfgCZpvbTSmhaWvLRj9e1ezUDBEPizGgshgm3ea1T5TCNEEBq0roauu7BPuRZjA3wO4KqA==}
|
||||||
|
|
||||||
markdown-to-jsx@7.7.17:
|
|
||||||
resolution: {integrity: sha512-7mG/1feQ0TX5I7YyMZVDgCC/y2I3CiEhIRQIhyov9nGBP5eoVrOXXHuL5ZP8GRfxVZKRiXWJgwXkb9It+nQZfQ==}
|
|
||||||
engines: {node: '>= 10'}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>= 0.14.0'
|
|
||||||
peerDependenciesMeta:
|
|
||||||
react:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
marked@17.0.6:
|
marked@17.0.6:
|
||||||
resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==}
|
resolution: {integrity: sha512-gB0gkNafnonOw0obSTEGZTT86IuhILt2Wfx0mWH/1Au83kybTayroZ/V6nS25mN7u8ASy+5fMhgB3XPNrOZdmA==}
|
||||||
engines: {node: '>= 20'}
|
engines: {node: '>= 20'}
|
||||||
|
|
@ -4866,12 +4709,6 @@ packages:
|
||||||
mdn-data@2.27.1:
|
mdn-data@2.27.1:
|
||||||
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==}
|
||||||
|
|
||||||
meilisearch@0.53.0:
|
|
||||||
resolution: {integrity: sha512-nG4VXbEOSzUmtbfsgOo+t6yX1ECEgXaT4hC0ap9MBpQGK5xwT+NWYDENYsKWR75cVaWaAqva+ok4zHlgtdXlLw==}
|
|
||||||
|
|
||||||
meilisearch@0.54.0:
|
|
||||||
resolution: {integrity: sha512-b1bwJAEfj8C6hgSN88+/LvW3pe3nWC+thBS2seAbPZGakf/vzsLqppgZquiomzCr2GhU7U7H289625qhYe3rbw==}
|
|
||||||
|
|
||||||
merge-stream@2.0.0:
|
merge-stream@2.0.0:
|
||||||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
|
||||||
|
|
||||||
|
|
@ -4931,16 +4768,9 @@ packages:
|
||||||
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
|
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
|
||||||
engines: {node: '>= 18'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
mitt@2.1.0:
|
|
||||||
resolution: {integrity: sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==}
|
|
||||||
|
|
||||||
mitt@3.0.1:
|
mitt@3.0.1:
|
||||||
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
|
||||||
|
|
||||||
mkdirp@0.3.0:
|
|
||||||
resolution: {integrity: sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==}
|
|
||||||
deprecated: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)
|
|
||||||
|
|
||||||
mlly@1.8.2:
|
mlly@1.8.2:
|
||||||
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==}
|
||||||
|
|
||||||
|
|
@ -5027,10 +4857,6 @@ packages:
|
||||||
node-releases@2.0.38:
|
node-releases@2.0.38:
|
||||||
resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==}
|
resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==}
|
||||||
|
|
||||||
nopt@1.0.10:
|
|
||||||
resolution: {integrity: sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
nopt@8.1.0:
|
nopt@8.1.0:
|
||||||
resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
|
resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==}
|
||||||
engines: {node: ^18.17.0 || >=20.5.0}
|
engines: {node: ^18.17.0 || >=20.5.0}
|
||||||
|
|
@ -5051,9 +4877,6 @@ packages:
|
||||||
nth-check@2.1.1:
|
nth-check@2.1.1:
|
||||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||||
|
|
||||||
nuxt-meilisearch@1.4.17:
|
|
||||||
resolution: {integrity: sha512-9O0dUIwuu00YAGVqQ0BVp/7kgLf3a6ONeOy6gDYtgj8sw/VI1o5tYIwg9hMjesOD1Jw/2WGL8dORkNRN2Mwedg==}
|
|
||||||
|
|
||||||
nuxt@4.4.5:
|
nuxt@4.4.5:
|
||||||
resolution: {integrity: sha512-MwTf3wyaEIm1U9/T1VKpqg7rGhhrn5Cx2ZS40lwo8GxsiY9xE7UOj5Cg0eAI0fSbJzyXlzdxspytgqWsgL+nIA==}
|
resolution: {integrity: sha512-MwTf3wyaEIm1U9/T1VKpqg7rGhhrn5Cx2ZS40lwo8GxsiY9xE7UOj5Cg0eAI0fSbJzyXlzdxspytgqWsgL+nIA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
|
@ -5431,9 +5254,6 @@ packages:
|
||||||
resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
|
resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
|
||||||
engines: {node: '>=20'}
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
preact@10.29.1:
|
|
||||||
resolution: {integrity: sha512-gQCLc/vWroE8lIpleXtdJhTFDogTdZG9AjMUpVkDf2iTCNwYNWA+u16dL41TqUDJO4gm2IgrcMv3uTpjd4Pwmg==}
|
|
||||||
|
|
||||||
prelude-ls@1.2.1:
|
prelude-ls@1.2.1:
|
||||||
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
@ -5499,10 +5319,6 @@ packages:
|
||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
qs@6.9.9:
|
|
||||||
resolution: {integrity: sha512-4mp+ySf3n7scxSqU6LIO9jjYCJWIdbXh3EQ8uvEc1uXiZm8sLI7moRTOvZb66c8zEHelBGEVRkSW6e9JCaayTw==}
|
|
||||||
engines: {node: '>=0.6'}
|
|
||||||
|
|
||||||
quansync@0.2.11:
|
quansync@0.2.11:
|
||||||
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
|
||||||
|
|
||||||
|
|
@ -5519,16 +5335,9 @@ packages:
|
||||||
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
rc9@2.1.2:
|
|
||||||
resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==}
|
|
||||||
|
|
||||||
rc9@3.0.1:
|
rc9@3.0.1:
|
||||||
resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==}
|
resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==}
|
||||||
|
|
||||||
react@19.2.6:
|
|
||||||
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
readable-stream@2.3.8:
|
readable-stream@2.3.8:
|
||||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||||
|
|
||||||
|
|
@ -5667,9 +5476,6 @@ packages:
|
||||||
scule@1.3.0:
|
scule@1.3.0:
|
||||||
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
|
resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==}
|
||||||
|
|
||||||
search-insights@2.17.3:
|
|
||||||
resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==}
|
|
||||||
|
|
||||||
semver@6.3.1:
|
semver@6.3.1:
|
||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
@ -6342,19 +6148,6 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.0.0
|
vue: ^3.0.0
|
||||||
|
|
||||||
vue-instantsearch@4.22.0:
|
|
||||||
resolution: {integrity: sha512-/BUdzD+NAybC81FGzEeSuCSYobYUVgHEHI7QY0HlTzdxIHOqWlw2y152/80kGKndS0C+Yu5ifGGg+xVIcFc4Dg==}
|
|
||||||
peerDependencies:
|
|
||||||
'@vue/server-renderer': ^3.1.2
|
|
||||||
algoliasearch: '>= 3.32.0 < 6'
|
|
||||||
vue: ^2.6.0 || >=3.0.0-rc.0
|
|
||||||
vue-server-renderer: ^2.6.11
|
|
||||||
peerDependenciesMeta:
|
|
||||||
'@vue/server-renderer':
|
|
||||||
optional: true
|
|
||||||
vue-server-renderer:
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
vue-router@4.6.4:
|
vue-router@4.6.4:
|
||||||
resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
|
resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -6519,120 +6312,11 @@ packages:
|
||||||
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
|
resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
zod-to-json-schema@3.24.6:
|
|
||||||
resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==}
|
|
||||||
peerDependencies:
|
|
||||||
zod: ^3.24.1
|
|
||||||
|
|
||||||
zod@4.4.3:
|
zod@4.4.3:
|
||||||
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
|
resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@ai-sdk/gateway@2.0.88(zod@4.4.3)':
|
|
||||||
dependencies:
|
|
||||||
'@ai-sdk/provider': 2.0.3
|
|
||||||
'@ai-sdk/provider-utils': 3.0.25(zod@4.4.3)
|
|
||||||
'@vercel/oidc': 3.1.0
|
|
||||||
zod: 4.4.3
|
|
||||||
|
|
||||||
'@ai-sdk/provider-utils@3.0.25(zod@4.4.3)':
|
|
||||||
dependencies:
|
|
||||||
'@ai-sdk/provider': 2.0.3
|
|
||||||
'@standard-schema/spec': 1.1.0
|
|
||||||
eventsource-parser: 3.0.8
|
|
||||||
zod: 4.4.3
|
|
||||||
|
|
||||||
'@ai-sdk/provider@2.0.3':
|
|
||||||
dependencies:
|
|
||||||
json-schema: 0.4.0
|
|
||||||
|
|
||||||
'@algolia/abtesting@1.18.1':
|
|
||||||
dependencies:
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
'@algolia/requester-browser-xhr': 5.52.1
|
|
||||||
'@algolia/requester-fetch': 5.52.1
|
|
||||||
'@algolia/requester-node-http': 5.52.1
|
|
||||||
|
|
||||||
'@algolia/client-abtesting@5.52.1':
|
|
||||||
dependencies:
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
'@algolia/requester-browser-xhr': 5.52.1
|
|
||||||
'@algolia/requester-fetch': 5.52.1
|
|
||||||
'@algolia/requester-node-http': 5.52.1
|
|
||||||
|
|
||||||
'@algolia/client-analytics@5.52.1':
|
|
||||||
dependencies:
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
'@algolia/requester-browser-xhr': 5.52.1
|
|
||||||
'@algolia/requester-fetch': 5.52.1
|
|
||||||
'@algolia/requester-node-http': 5.52.1
|
|
||||||
|
|
||||||
'@algolia/client-common@5.52.1': {}
|
|
||||||
|
|
||||||
'@algolia/client-insights@5.52.1':
|
|
||||||
dependencies:
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
'@algolia/requester-browser-xhr': 5.52.1
|
|
||||||
'@algolia/requester-fetch': 5.52.1
|
|
||||||
'@algolia/requester-node-http': 5.52.1
|
|
||||||
|
|
||||||
'@algolia/client-personalization@5.52.1':
|
|
||||||
dependencies:
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
'@algolia/requester-browser-xhr': 5.52.1
|
|
||||||
'@algolia/requester-fetch': 5.52.1
|
|
||||||
'@algolia/requester-node-http': 5.52.1
|
|
||||||
|
|
||||||
'@algolia/client-query-suggestions@5.52.1':
|
|
||||||
dependencies:
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
'@algolia/requester-browser-xhr': 5.52.1
|
|
||||||
'@algolia/requester-fetch': 5.52.1
|
|
||||||
'@algolia/requester-node-http': 5.52.1
|
|
||||||
|
|
||||||
'@algolia/client-search@5.52.1':
|
|
||||||
dependencies:
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
'@algolia/requester-browser-xhr': 5.52.1
|
|
||||||
'@algolia/requester-fetch': 5.52.1
|
|
||||||
'@algolia/requester-node-http': 5.52.1
|
|
||||||
|
|
||||||
'@algolia/events@4.0.1': {}
|
|
||||||
|
|
||||||
'@algolia/ingestion@1.52.1':
|
|
||||||
dependencies:
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
'@algolia/requester-browser-xhr': 5.52.1
|
|
||||||
'@algolia/requester-fetch': 5.52.1
|
|
||||||
'@algolia/requester-node-http': 5.52.1
|
|
||||||
|
|
||||||
'@algolia/monitoring@1.52.1':
|
|
||||||
dependencies:
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
'@algolia/requester-browser-xhr': 5.52.1
|
|
||||||
'@algolia/requester-fetch': 5.52.1
|
|
||||||
'@algolia/requester-node-http': 5.52.1
|
|
||||||
|
|
||||||
'@algolia/recommend@5.52.1':
|
|
||||||
dependencies:
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
'@algolia/requester-browser-xhr': 5.52.1
|
|
||||||
'@algolia/requester-fetch': 5.52.1
|
|
||||||
'@algolia/requester-node-http': 5.52.1
|
|
||||||
|
|
||||||
'@algolia/requester-browser-xhr@5.52.1':
|
|
||||||
dependencies:
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
|
|
||||||
'@algolia/requester-fetch@5.52.1':
|
|
||||||
dependencies:
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
|
|
||||||
'@algolia/requester-node-http@5.52.1':
|
|
||||||
dependencies:
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
|
|
||||||
'@antfu/install-pkg@1.1.0':
|
'@antfu/install-pkg@1.1.0':
|
||||||
|
|
@ -7457,10 +7141,6 @@ snapshots:
|
||||||
|
|
||||||
'@mapbox/whoots-js@3.1.0': {}
|
'@mapbox/whoots-js@3.1.0': {}
|
||||||
|
|
||||||
'@meilisearch/instant-meilisearch@0.28.0':
|
|
||||||
dependencies:
|
|
||||||
meilisearch: 0.53.0
|
|
||||||
|
|
||||||
'@miyaneee/rollup-plugin-json5@1.2.0(rollup@4.60.3)':
|
'@miyaneee/rollup-plugin-json5@1.2.0(rollup@4.60.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.3)
|
'@rollup/pluginutils': 5.3.0(rollup@4.60.3)
|
||||||
|
|
@ -7748,31 +7428,6 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- magicast
|
- magicast
|
||||||
|
|
||||||
'@nuxt/kit@4.2.0(magicast@0.5.2)':
|
|
||||||
dependencies:
|
|
||||||
c12: 3.3.4(magicast@0.5.2)
|
|
||||||
consola: 3.4.2
|
|
||||||
defu: 6.1.7
|
|
||||||
destr: 2.0.5
|
|
||||||
errx: 0.1.0
|
|
||||||
exsolve: 1.0.8
|
|
||||||
ignore: 7.0.5
|
|
||||||
jiti: 2.7.0
|
|
||||||
klona: 2.0.6
|
|
||||||
mlly: 1.8.2
|
|
||||||
ohash: 2.0.11
|
|
||||||
pathe: 2.0.3
|
|
||||||
pkg-types: 2.3.1
|
|
||||||
rc9: 2.1.2
|
|
||||||
scule: 1.3.0
|
|
||||||
semver: 7.8.0
|
|
||||||
tinyglobby: 0.2.16
|
|
||||||
ufo: 1.6.4
|
|
||||||
unctx: 2.5.0
|
|
||||||
untyped: 2.0.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- magicast
|
|
||||||
|
|
||||||
'@nuxt/kit@4.4.5(magicast@0.5.2)':
|
'@nuxt/kit@4.4.5(magicast@0.5.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
c12: 3.3.4(magicast@0.5.2)
|
c12: 3.3.4(magicast@0.5.2)
|
||||||
|
|
@ -7885,7 +7540,7 @@ snapshots:
|
||||||
rc9: 3.0.1
|
rc9: 3.0.1
|
||||||
std-env: 4.1.0
|
std-env: 4.1.0
|
||||||
|
|
||||||
'@nuxt/ui@4.7.1(@internationalized/date@3.12.1)(@internationalized/number@3.6.6)(@tiptap/extensions@3.23.1(@tiptap/core@3.23.1(@tiptap/pm@3.23.1))(@tiptap/pm@3.23.1))(@tiptap/y-tiptap@3.0.3(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30))(axios@1.16.0)(change-case@5.4.4)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.10.1)(magicast@0.5.2)(react@19.2.6)(tailwindcss@4.3.0)(typescript@6.0.3)(vite@7.3.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0))(vue-router@4.6.4(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))(yjs@13.6.30)(zod@4.4.3)':
|
'@nuxt/ui@4.7.1(@internationalized/date@3.12.1)(@internationalized/number@3.6.6)(@tiptap/extensions@3.23.1(@tiptap/core@3.23.1(@tiptap/pm@3.23.1))(@tiptap/pm@3.23.1))(@tiptap/y-tiptap@3.0.3(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.8)(y-protocols@1.0.7(yjs@13.6.30))(yjs@13.6.30))(axios@1.16.0)(change-case@5.4.4)(db0@0.3.4)(embla-carousel@8.6.0)(ioredis@5.10.1)(magicast@0.5.2)(tailwindcss@4.3.0)(typescript@6.0.3)(vite@7.3.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0))(vue-router@4.6.4(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))(yjs@13.6.30)(zod@4.4.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@floating-ui/dom': 1.7.6
|
'@floating-ui/dom': 1.7.6
|
||||||
'@iconify/vue': 5.0.1(vue@3.5.34(typescript@6.0.3))
|
'@iconify/vue': 5.0.1(vue@3.5.34(typescript@6.0.3))
|
||||||
|
|
@ -7935,7 +7590,7 @@ snapshots:
|
||||||
knitwork: 1.3.0
|
knitwork: 1.3.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
mlly: 1.8.2
|
mlly: 1.8.2
|
||||||
motion-v: 2.2.1(@vueuse/core@14.3.0(vue@3.5.34(typescript@6.0.3)))(react@19.2.6)(vue@3.5.34(typescript@6.0.3))
|
motion-v: 2.2.1(@vueuse/core@14.3.0(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3))
|
||||||
ohash: 2.0.11
|
ohash: 2.0.11
|
||||||
pathe: 2.0.3
|
pathe: 2.0.3
|
||||||
reka-ui: 2.9.6(vue@3.5.34(typescript@6.0.3))
|
reka-ui: 2.9.6(vue@3.5.34(typescript@6.0.3))
|
||||||
|
|
@ -8103,8 +7758,6 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
- vue
|
- vue
|
||||||
|
|
||||||
'@opentelemetry/api@1.9.0': {}
|
|
||||||
|
|
||||||
'@oxc-minify/binding-android-arm-eabi@0.128.0':
|
'@oxc-minify/binding-android-arm-eabi@0.128.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -9080,8 +8733,6 @@ snapshots:
|
||||||
|
|
||||||
'@types/dagre@0.7.54': {}
|
'@types/dagre@0.7.54': {}
|
||||||
|
|
||||||
'@types/dom-speech-recognition@0.0.1': {}
|
|
||||||
|
|
||||||
'@types/esrecurse@4.3.1': {}
|
'@types/esrecurse@4.3.1': {}
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
|
@ -9090,10 +8741,6 @@ snapshots:
|
||||||
|
|
||||||
'@types/geojson@7946.0.16': {}
|
'@types/geojson@7946.0.16': {}
|
||||||
|
|
||||||
'@types/google.maps@3.64.0': {}
|
|
||||||
|
|
||||||
'@types/hogan.js@3.0.5': {}
|
|
||||||
|
|
||||||
'@types/json-schema@7.0.15': {}
|
'@types/json-schema@7.0.15': {}
|
||||||
|
|
||||||
'@types/leaflet@1.7.6':
|
'@types/leaflet@1.7.6':
|
||||||
|
|
@ -9112,8 +8759,6 @@ snapshots:
|
||||||
|
|
||||||
'@types/pbf@3.0.5': {}
|
'@types/pbf@3.0.5': {}
|
||||||
|
|
||||||
'@types/qs@6.15.1': {}
|
|
||||||
|
|
||||||
'@types/resolve@1.20.2': {}
|
'@types/resolve@1.20.2': {}
|
||||||
|
|
||||||
'@types/supercluster@5.0.3':
|
'@types/supercluster@5.0.3':
|
||||||
|
|
@ -9445,8 +9090,6 @@ snapshots:
|
||||||
- rollup
|
- rollup
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vercel/oidc@3.1.0': {}
|
|
||||||
|
|
||||||
'@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))':
|
'@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.0
|
'@babel/core': 7.29.0
|
||||||
|
|
@ -9683,8 +9326,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
vue: 3.5.34(typescript@6.0.3)
|
vue: 3.5.34(typescript@6.0.3)
|
||||||
|
|
||||||
abbrev@1.1.1: {}
|
|
||||||
|
|
||||||
abbrev@3.0.1: {}
|
abbrev@3.0.1: {}
|
||||||
|
|
||||||
abort-controller@3.0.0:
|
abort-controller@3.0.0:
|
||||||
|
|
@ -9703,14 +9344,6 @@ snapshots:
|
||||||
|
|
||||||
agent-base@7.1.4: {}
|
agent-base@7.1.4: {}
|
||||||
|
|
||||||
ai@5.0.186(zod@4.4.3):
|
|
||||||
dependencies:
|
|
||||||
'@ai-sdk/gateway': 2.0.88(zod@4.4.3)
|
|
||||||
'@ai-sdk/provider': 2.0.3
|
|
||||||
'@ai-sdk/provider-utils': 3.0.25(zod@4.4.3)
|
|
||||||
'@opentelemetry/api': 1.9.0
|
|
||||||
zod: 4.4.3
|
|
||||||
|
|
||||||
ajv@6.15.0:
|
ajv@6.15.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
|
|
@ -9718,28 +9351,6 @@ snapshots:
|
||||||
json-schema-traverse: 0.4.1
|
json-schema-traverse: 0.4.1
|
||||||
uri-js: 4.4.1
|
uri-js: 4.4.1
|
||||||
|
|
||||||
algoliasearch-helper@3.26.0(algoliasearch@5.52.1):
|
|
||||||
dependencies:
|
|
||||||
'@algolia/events': 4.0.1
|
|
||||||
algoliasearch: 5.52.1
|
|
||||||
|
|
||||||
algoliasearch@5.52.1:
|
|
||||||
dependencies:
|
|
||||||
'@algolia/abtesting': 1.18.1
|
|
||||||
'@algolia/client-abtesting': 5.52.1
|
|
||||||
'@algolia/client-analytics': 5.52.1
|
|
||||||
'@algolia/client-common': 5.52.1
|
|
||||||
'@algolia/client-insights': 5.52.1
|
|
||||||
'@algolia/client-personalization': 5.52.1
|
|
||||||
'@algolia/client-query-suggestions': 5.52.1
|
|
||||||
'@algolia/client-search': 5.52.1
|
|
||||||
'@algolia/ingestion': 1.52.1
|
|
||||||
'@algolia/monitoring': 1.52.1
|
|
||||||
'@algolia/recommend': 5.52.1
|
|
||||||
'@algolia/requester-browser-xhr': 5.52.1
|
|
||||||
'@algolia/requester-fetch': 5.52.1
|
|
||||||
'@algolia/requester-node-http': 5.52.1
|
|
||||||
|
|
||||||
alien-signals@3.1.2: {}
|
alien-signals@3.1.2: {}
|
||||||
|
|
||||||
ansi-regex@5.0.1: {}
|
ansi-regex@5.0.1: {}
|
||||||
|
|
@ -10833,8 +10444,6 @@ snapshots:
|
||||||
|
|
||||||
events@3.3.0: {}
|
events@3.3.0: {}
|
||||||
|
|
||||||
eventsource-parser@3.0.8: {}
|
|
||||||
|
|
||||||
execa@8.0.1:
|
execa@8.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
|
|
@ -10985,13 +10594,11 @@ snapshots:
|
||||||
|
|
||||||
fraction.js@5.3.4: {}
|
fraction.js@5.3.4: {}
|
||||||
|
|
||||||
framer-motion@12.38.0(react@19.2.6):
|
framer-motion@12.38.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
motion-dom: 12.38.0
|
motion-dom: 12.38.0
|
||||||
motion-utils: 12.36.0
|
motion-utils: 12.36.0
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optionalDependencies:
|
|
||||||
react: 19.2.6
|
|
||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
|
|
||||||
|
|
@ -11124,17 +10731,10 @@ snapshots:
|
||||||
|
|
||||||
hey-listen@1.0.8: {}
|
hey-listen@1.0.8: {}
|
||||||
|
|
||||||
hogan.js@3.0.2:
|
|
||||||
dependencies:
|
|
||||||
mkdirp: 0.3.0
|
|
||||||
nopt: 1.0.10
|
|
||||||
|
|
||||||
hookable@5.5.3: {}
|
hookable@5.5.3: {}
|
||||||
|
|
||||||
hookable@6.1.1: {}
|
hookable@6.1.1: {}
|
||||||
|
|
||||||
htm@3.1.1: {}
|
|
||||||
|
|
||||||
html-entities@2.6.0: {}
|
html-entities@2.6.0: {}
|
||||||
|
|
||||||
http-errors@2.0.1:
|
http-errors@2.0.1:
|
||||||
|
|
@ -11195,38 +10795,6 @@ snapshots:
|
||||||
|
|
||||||
ini@4.1.1: {}
|
ini@4.1.1: {}
|
||||||
|
|
||||||
instantsearch-ui-components@0.12.0(react@19.2.6):
|
|
||||||
dependencies:
|
|
||||||
'@babel/runtime': 7.29.2
|
|
||||||
ai: 5.0.186(zod@4.4.3)
|
|
||||||
markdown-to-jsx: 7.7.17(react@19.2.6)
|
|
||||||
zod: 4.4.3
|
|
||||||
zod-to-json-schema: 3.24.6(zod@4.4.3)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- react
|
|
||||||
|
|
||||||
instantsearch.css@8.6.0: {}
|
|
||||||
|
|
||||||
instantsearch.js@4.81.0(algoliasearch@5.52.1):
|
|
||||||
dependencies:
|
|
||||||
'@algolia/events': 4.0.1
|
|
||||||
'@types/dom-speech-recognition': 0.0.1
|
|
||||||
'@types/google.maps': 3.64.0
|
|
||||||
'@types/hogan.js': 3.0.5
|
|
||||||
'@types/qs': 6.15.1
|
|
||||||
ai: 5.0.186(zod@4.4.3)
|
|
||||||
algoliasearch: 5.52.1
|
|
||||||
algoliasearch-helper: 3.26.0(algoliasearch@5.52.1)
|
|
||||||
hogan.js: 3.0.2
|
|
||||||
htm: 3.1.1
|
|
||||||
instantsearch-ui-components: 0.12.0(react@19.2.6)
|
|
||||||
preact: 10.29.1
|
|
||||||
qs: 6.9.9
|
|
||||||
react: 19.2.6
|
|
||||||
search-insights: 2.17.3
|
|
||||||
zod: 4.4.3
|
|
||||||
zod-to-json-schema: 3.24.6(zod@4.4.3)
|
|
||||||
|
|
||||||
internmap@1.0.1: {}
|
internmap@1.0.1: {}
|
||||||
|
|
||||||
internmap@2.0.3: {}
|
internmap@2.0.3: {}
|
||||||
|
|
@ -11337,8 +10905,6 @@ snapshots:
|
||||||
|
|
||||||
json-schema-traverse@0.4.1: {}
|
json-schema-traverse@0.4.1: {}
|
||||||
|
|
||||||
json-schema@0.4.0: {}
|
|
||||||
|
|
||||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||||
|
|
||||||
json5@2.2.3: {}
|
json5@2.2.3: {}
|
||||||
|
|
@ -11555,10 +11121,6 @@ snapshots:
|
||||||
tinyqueue: 2.0.3
|
tinyqueue: 2.0.3
|
||||||
vt-pbf: 3.1.3
|
vt-pbf: 3.1.3
|
||||||
|
|
||||||
markdown-to-jsx@7.7.17(react@19.2.6):
|
|
||||||
optionalDependencies:
|
|
||||||
react: 19.2.6
|
|
||||||
|
|
||||||
marked@17.0.6: {}
|
marked@17.0.6: {}
|
||||||
|
|
||||||
math-intrinsics@1.1.0: {}
|
math-intrinsics@1.1.0: {}
|
||||||
|
|
@ -11567,10 +11129,6 @@ snapshots:
|
||||||
|
|
||||||
mdn-data@2.27.1: {}
|
mdn-data@2.27.1: {}
|
||||||
|
|
||||||
meilisearch@0.53.0: {}
|
|
||||||
|
|
||||||
meilisearch@0.54.0: {}
|
|
||||||
|
|
||||||
merge-stream@2.0.0: {}
|
merge-stream@2.0.0: {}
|
||||||
|
|
||||||
merge2@1.4.1: {}
|
merge2@1.4.1: {}
|
||||||
|
|
@ -11616,12 +11174,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
minipass: 7.1.3
|
minipass: 7.1.3
|
||||||
|
|
||||||
mitt@2.1.0: {}
|
|
||||||
|
|
||||||
mitt@3.0.1: {}
|
mitt@3.0.1: {}
|
||||||
|
|
||||||
mkdirp@0.3.0: {}
|
|
||||||
|
|
||||||
mlly@1.8.2:
|
mlly@1.8.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
acorn: 8.16.0
|
acorn: 8.16.0
|
||||||
|
|
@ -11637,10 +11191,10 @@ snapshots:
|
||||||
|
|
||||||
motion-utils@12.36.0: {}
|
motion-utils@12.36.0: {}
|
||||||
|
|
||||||
motion-v@2.2.1(@vueuse/core@14.3.0(vue@3.5.34(typescript@6.0.3)))(react@19.2.6)(vue@3.5.34(typescript@6.0.3)):
|
motion-v@2.2.1(@vueuse/core@14.3.0(vue@3.5.34(typescript@6.0.3)))(vue@3.5.34(typescript@6.0.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vueuse/core': 14.3.0(vue@3.5.34(typescript@6.0.3))
|
'@vueuse/core': 14.3.0(vue@3.5.34(typescript@6.0.3))
|
||||||
framer-motion: 12.38.0(react@19.2.6)
|
framer-motion: 12.38.0
|
||||||
hey-listen: 1.0.8
|
hey-listen: 1.0.8
|
||||||
motion-dom: 12.38.0
|
motion-dom: 12.38.0
|
||||||
motion-utils: 12.36.0
|
motion-utils: 12.36.0
|
||||||
|
|
@ -11787,10 +11341,6 @@ snapshots:
|
||||||
|
|
||||||
node-releases@2.0.38: {}
|
node-releases@2.0.38: {}
|
||||||
|
|
||||||
nopt@1.0.10:
|
|
||||||
dependencies:
|
|
||||||
abbrev: 1.1.1
|
|
||||||
|
|
||||||
nopt@8.1.0:
|
nopt@8.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
abbrev: 3.0.1
|
abbrev: 3.0.1
|
||||||
|
|
@ -11810,21 +11360,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
boolbase: 1.0.0
|
boolbase: 1.0.0
|
||||||
|
|
||||||
nuxt-meilisearch@1.4.17(@vue/server-renderer@3.5.34(vue@3.5.34(typescript@6.0.3)))(algoliasearch@5.52.1)(magicast@0.5.2)(react@19.2.6)(vue@3.5.34(typescript@6.0.3)):
|
|
||||||
dependencies:
|
|
||||||
'@meilisearch/instant-meilisearch': 0.28.0
|
|
||||||
'@nuxt/kit': 4.2.0(magicast@0.5.2)
|
|
||||||
instantsearch.css: 8.6.0
|
|
||||||
meilisearch: 0.54.0
|
|
||||||
vue-instantsearch: 4.22.0(@vue/server-renderer@3.5.34(vue@3.5.34(typescript@6.0.3)))(algoliasearch@5.52.1)(react@19.2.6)(vue@3.5.34(typescript@6.0.3))
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@vue/server-renderer'
|
|
||||||
- algoliasearch
|
|
||||||
- magicast
|
|
||||||
- react
|
|
||||||
- vue
|
|
||||||
- vue-server-renderer
|
|
||||||
|
|
||||||
nuxt@4.4.5(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@vue/compiler-sfc@3.5.34)(cac@6.7.14)(db0@0.3.4)(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.2)(optionator@0.9.4)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(rollup-plugin-visualizer@7.0.1(rollup@4.60.3))(rollup@4.60.3)(srvx@0.11.15)(terser@5.47.1)(typescript@6.0.3)(vite@7.3.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.8(typescript@6.0.3))(yaml@2.9.0):
|
nuxt@4.4.5(@babel/core@7.29.0)(@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0))(@parcel/watcher@2.5.6)(@vue/compiler-sfc@3.5.34)(cac@6.7.14)(db0@0.3.4)(eslint@10.3.0(jiti@2.7.0))(ioredis@5.10.1)(lightningcss@1.32.0)(magicast@0.5.2)(optionator@0.9.4)(pinia@3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)))(rollup-plugin-visualizer@7.0.1(rollup@4.60.3))(rollup@4.60.3)(srvx@0.11.15)(terser@5.47.1)(typescript@6.0.3)(vite@7.3.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.47.1)(yaml@2.9.0))(vue-tsc@3.2.8(typescript@6.0.3))(yaml@2.9.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@dxup/nuxt': 0.4.1(magicast@0.5.2)(typescript@6.0.3)
|
'@dxup/nuxt': 0.4.1(magicast@0.5.2)(typescript@6.0.3)
|
||||||
|
|
@ -12375,8 +11910,6 @@ snapshots:
|
||||||
|
|
||||||
powershell-utils@0.1.0: {}
|
powershell-utils@0.1.0: {}
|
||||||
|
|
||||||
preact@10.29.1: {}
|
|
||||||
|
|
||||||
prelude-ls@1.2.1: {}
|
prelude-ls@1.2.1: {}
|
||||||
|
|
||||||
pretty-bytes@7.1.0: {}
|
pretty-bytes@7.1.0: {}
|
||||||
|
|
@ -12466,8 +11999,6 @@ snapshots:
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
|
|
||||||
qs@6.9.9: {}
|
|
||||||
|
|
||||||
quansync@0.2.11: {}
|
quansync@0.2.11: {}
|
||||||
|
|
||||||
queue-microtask@1.2.3: {}
|
queue-microtask@1.2.3: {}
|
||||||
|
|
@ -12478,18 +12009,11 @@ snapshots:
|
||||||
|
|
||||||
range-parser@1.2.1: {}
|
range-parser@1.2.1: {}
|
||||||
|
|
||||||
rc9@2.1.2:
|
|
||||||
dependencies:
|
|
||||||
defu: 6.1.7
|
|
||||||
destr: 2.0.5
|
|
||||||
|
|
||||||
rc9@3.0.1:
|
rc9@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
defu: 6.1.7
|
defu: 6.1.7
|
||||||
destr: 2.0.5
|
destr: 2.0.5
|
||||||
|
|
||||||
react@19.2.6: {}
|
|
||||||
|
|
||||||
readable-stream@2.3.8:
|
readable-stream@2.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
core-util-is: 1.0.3
|
core-util-is: 1.0.3
|
||||||
|
|
@ -12648,8 +12172,6 @@ snapshots:
|
||||||
|
|
||||||
scule@1.3.0: {}
|
scule@1.3.0: {}
|
||||||
|
|
||||||
search-insights@2.17.3: {}
|
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
semver@7.8.0: {}
|
semver@7.8.0: {}
|
||||||
|
|
@ -13331,18 +12853,6 @@ snapshots:
|
||||||
'@vue/devtools-api': 6.6.4
|
'@vue/devtools-api': 6.6.4
|
||||||
vue: 3.5.34(typescript@6.0.3)
|
vue: 3.5.34(typescript@6.0.3)
|
||||||
|
|
||||||
vue-instantsearch@4.22.0(@vue/server-renderer@3.5.34(vue@3.5.34(typescript@6.0.3)))(algoliasearch@5.52.1)(react@19.2.6)(vue@3.5.34(typescript@6.0.3)):
|
|
||||||
dependencies:
|
|
||||||
algoliasearch: 5.52.1
|
|
||||||
instantsearch-ui-components: 0.12.0(react@19.2.6)
|
|
||||||
instantsearch.js: 4.81.0(algoliasearch@5.52.1)
|
|
||||||
mitt: 2.1.0
|
|
||||||
vue: 3.5.34(typescript@6.0.3)
|
|
||||||
optionalDependencies:
|
|
||||||
'@vue/server-renderer': 3.5.34(vue@3.5.34(typescript@6.0.3))
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- react
|
|
||||||
|
|
||||||
vue-router@4.6.4(vue@3.5.34(typescript@6.0.3)):
|
vue-router@4.6.4(vue@3.5.34(typescript@6.0.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-api': 6.6.4
|
'@vue/devtools-api': 6.6.4
|
||||||
|
|
@ -13504,8 +13014,4 @@ snapshots:
|
||||||
compress-commons: 6.0.2
|
compress-commons: 6.0.2
|
||||||
readable-stream: 4.7.0
|
readable-stream: 4.7.0
|
||||||
|
|
||||||
zod-to-json-schema@3.24.6(zod@4.4.3):
|
|
||||||
dependencies:
|
|
||||||
zod: 4.4.3
|
|
||||||
|
|
||||||
zod@4.4.3: {}
|
zod@4.4.3: {}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
allowBuilds:
|
||||||
|
'@parcel/watcher': true
|
||||||
|
esbuild: true
|
||||||
|
maplibre-gl: true
|
||||||
|
unrs-resolver: true
|
||||||
|
vue-demi: true
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g>
|
||||||
|
<clipPath id="_clip1">
|
||||||
|
<circle cx="500" cy="500" r="470"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#_clip1)">
|
||||||
|
<g transform="matrix(41.330743,0,0,41.330743,-2116.926503,-46.100585)">
|
||||||
|
<path d="M88.473,25.492C79.652,22.367 63.422,23.441 59.117,25.473C77.457,13.191 100.926,2.789 128.516,0.344C134.094,-0.098 141.602,-0.145 148.871,0.402C183.086,2.969 210.52,21.004 226.969,43C211.91,29.102 187.027,10.215 147.359,10.215C116.781,10.215 93.781,22.441 88.473,25.492Z" style="fill:rgb(236,27,35);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(41.330743,0,0,41.330743,-2116.926503,-46.100585)">
|
||||||
|
<path d="M29.234,25.473C20.656,22.078 4.145,23.656 0.199,25.418C22.531,9.531 54.453,-1.191 101.211,0.512C57.047,3.617 32.789,23.152 29.234,25.473Z" style="fill:rgb(45,77,162);"/>
|
||||||
|
</g>
|
||||||
|
<g transform="matrix(41.330743,0,0,41.330743,-2116.926503,-46.100585)">
|
||||||
|
<path d="M59.117,25.535C50.535,22.141 33.18,23.711 29.234,25.473C51.57,9.586 79.883,-2.051 128.961,0.473C94.258,2.949 69.059,19.316 59.117,25.535Z" style="fill:rgb(254,254,254);"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -0,0 +1,47 @@
|
||||||
|
import { defineEventHandler, readBody, createError } from 'h3'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const { message, recaptchaToken } = await readBody(event)
|
||||||
|
|
||||||
|
if (!message?.trim()) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Mensaje vacío' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = useRuntimeConfig(event)
|
||||||
|
const webhookUrl = config.public.feedbackWebhook
|
||||||
|
const feedbackToken = config.feedbackToken
|
||||||
|
|
||||||
|
if (!webhookUrl) {
|
||||||
|
console.error('[feedback] Webhook URL no configurada')
|
||||||
|
throw createError({ statusCode: 500, message: 'Webhook no configurado' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
if (feedbackToken) {
|
||||||
|
headers['X-Feedback-Token'] = feedbackToken
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await $fetch(webhookUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: { message, recaptchaToken },
|
||||||
|
retry: 1,
|
||||||
|
timeout: 10000
|
||||||
|
})
|
||||||
|
return { ok: true, data: res }
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('[feedback] Error al reenviar al webhook:', {
|
||||||
|
url: webhookUrl,
|
||||||
|
status: e?.response?.status,
|
||||||
|
statusText: e?.response?.statusText,
|
||||||
|
message: e?.message
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusCode = e?.response?.status || 502
|
||||||
|
const detail = e?.response?.statusText || e?.message || 'Error al reenviar el feedback'
|
||||||
|
throw createError({ statusCode, message: detail })
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue