Adding page component to document results
Adding aside to handle notes and highlights in future
This commit is contained in:
Julio Ruiz 2026-05-26 12:48:06 -05:00
parent e2deaa8066
commit 1756aadaff
4 changed files with 309 additions and 243 deletions

View File

@ -755,29 +755,35 @@ function highlightTextNodes(root: HTMLElement, terms: string[]): number {
return matches.length
}
const isPopOverOpen = ref(false)
const anchor = ref({ x: 0, y: 0 })
const virtualElement = computed(() => ({
getBoundingClientRect: () =>
({
width: 0,
height: 0,
left: anchor.value.x,
right: anchor.value.x,
top: anchor.value.y,
bottom: anchor.value.y,
...anchor.value
} as DOMRect)
}))
function handleSelection(event: MouseEvent) {
const selection = window.getSelection()
if (selection && selection.toString().length > 0) {
anchor.value = { x: event.clientX, y: event.clientY }
if (selection && selection.toString().length > 3) {
isPopOverOpen.value = true
} else {
isPopOverOpen.value = false
}
}
const links = computed(() => {
return [
{
label: 'Ver en sitio',
icon: 'ph-arrow-square-out',
to: matchUrl,
target: '_blank'
}
]
})
const items = computed(() => {
return [
{
label: 'Debug',
content: debugDocument(props.document)
}
]
})
</script>
<template>
@ -926,85 +932,104 @@ function handleSelection(event: MouseEvent) {
</div>
<div ref="scrollContainer" class="flex-1 overflow-y-auto relative bg-gray-100">
<!-- Cargando párrafos -->
<div v-if="paragraphsLoading" class="flex items-center justify-center gap-2 py-16 text-sm text-muted">
<UIcon name="i-lucide-loader-circle" class="size-5 animate-spin" />
Cargando párrafos...
</div>
<!-- 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">
<UPopover
v-model:open="isPopOverOpen"
:reference="virtualElement"
class="absolute top-[90vh] left-[900px]"
>
<!-- We use the slot to bind to our moving anchor -->
<template #anchor>
<div class="hidden" />
</template>
<template #content>
<UFieldGroup size="sm" orientation="horizontal" class="bg-white p-0 rounded-md shadow-md">
<UButton
icon="ph-note-pencil"
variant="ghost"
color="neutral"
size="lg"
@click="addNote()"
></UButton>
<UButton
icon="ph-highlighter"
variant="ghost"
color="neutral"
size="lg"
@click="highlightParagraph()"
></UButton>
<UButton
icon="ph-copy"
variant="ghost"
color="neutral"
size="lg"
@click="copyToClipboard()"
></UButton>
</UFieldGroup>
</template>
</UPopover>
<div class="bg-white rounded-lg shadow-md p-4 pl-2 sm:p-8 sm:pl-4 mb-4 last:mb-0">
<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-[32px_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="ghost"
class="text-gray-300 hover:text-white"
:class="(hit.document.type=='activities'?'hover:bg-carpagreen':'hover:bg-carpablue')"
@click="isPopOverOpen = !isPopOverOpen"
/>
</div>
<div class="">
<div
class="paragraph-html text-sm leading-relaxed text-gray-800 dark:text-gray-200"
@mouseup="handleSelection"
v-html="hit.document.html || hit.document.text"
/>
<UPage class="max-w-6xl mx-auto py-0 my-0 " @mouseup="handleSelection">
<UPageBody class="bg-white p-8 rounded-xl shadow-lg">
<UPageHeader
:title="document?.title"
:description="formatSignature(document)"
:headline="document?.draft ? 'Borrador' : ''"
:links="links"
class="py-0 pb-2"
/>
<div v-if="paragraphsLoading" class="flex items-center justify-center gap-2 py-16 text-sm text-muted">
<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>
<!-- Sin contenido -->
<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" />
<p>No hay contenido disponible para este documento.</p>
<p v-if="matchUrl">
Puedes
<ULink :to="matchUrl" target="_blank" class="text-primary">verlo en el sitio</ULink>.
</p>
</div>
<!-- Sin contenido -->
<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" />
<p>No hay contenido disponible para este documento.</p>
<p v-if="matchUrl">
Puedes
<ULink :to="matchUrl" target="_blank" class="text-primary">verlo en el sitio</ULink>.
</p>
</div>
</UPageBody>
<template #right>
<UPageAside
class="py-0 my-0"
>
<UFieldGroup orientation="horizontal" class="bg-white my-0 py-4 p-2 rounded-md shadow-md w-full justify-around">
<UTooltip
text="Crear nota"
>
<UButton
icon="ph-note-pencil"
:variant="isPopOverOpen ? 'solid': 'soft'"
:color="isPopOverOpen ? 'info' : 'neutral'"
size="lg"
:disabled="isPopOverOpen ? false : true"
@click="addNote()"
></UButton>
</UTooltip>
<UTooltip
text="Resaltar">
<UButton
icon="ph-highlighter"
:variant="isPopOverOpen ? 'solid': 'soft'"
:color="isPopOverOpen ? 'success' : 'neutral'"
size="lg"
:disabled="isPopOverOpen ? false : true"
@click="highlightParagraph()"
></UButton>
</UTooltip>
<UTooltip
text="Copiar">
<UButton
icon="ph-copy"
:variant="isPopOverOpen ? 'solid': 'soft'"
:color="isPopOverOpen ? 'warning' : 'neutral'"
size="lg"
:disabled="isPopOverOpen ? false : true"
@click="copyToClipboard()"
></UButton>
</UTooltip>
</UFieldGroup>
<UAccordion :items="items">
<template #body="{ item }">
<pre>{{ item.content }}</pre>
</template>
</UAccordion>
</UPageAside>
</template>
</UPage>
</div>
</UDashboardPanel>
</template>

View File

@ -432,7 +432,6 @@ async function fetchDocumentWithParagraphs(docId: string) {
selectedDocument.value = null
selectedParagraphs.value = []
try {
console.log(`[fetchDocumentWithParagraphs] Fetching document with paragraphs. collection=${props.mainCollection} docId=${docId}`)
const res = await documentsApi.multiSearch({
multiSearchParameters: {},
multiSearchSearchesParameter: {
@ -445,7 +444,6 @@ async function fetchDocumentWithParagraphs(docId: string) {
}]
}
})
console.log(`[fetchDocumentWithParagraphs] collection=${props.mainCollection} docId=${docId}`, JSON.stringify(res?.results?.[0], null, 2))
const hit = (res?.results?.[0] as { hits?: Array<{ document: Record<string, unknown> }> })?.hits?.[0]
if (hit) {
const docRaw = { ...hit.document }

View File

@ -1,171 +1,199 @@
import dayjs from "dayjs";
export interface ItemObject {
id: string;
date: number;
slug: string;
type: string;
place: string;
city: string;
state: string;
country: string;
thumbnail: string;
id: string;
date: number;
slug: string;
type: string;
place: string;
city: string;
state: string;
country: string;
thumbnail: string;
}
export interface FilesObject {
youtube?: string;
video?: string;
audio?: string;
booklet?: string;
simple?: string;
youtube?: string;
video?: string;
audio?: string;
booklet?: string;
simple?: string;
}
export function formatFiles( files:FilesObject ){
const { $i18n } = useNuxtApp();
const t = $i18n.t;
export function formatFiles(files: FilesObject) {
const { $i18n } = useNuxtApp();
const t = $i18n.t;
let items = []
if (files) {
if (files.youtube) {
items.push({
to: files.youtube,
target: '_blank',
label: 'Youtube',
icon: 'ph:youtube-logo-thin',
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'
})
}
let items = [];
if (files) {
if (files.youtube) {
items.push({
to: files.youtube,
target: "_blank",
label: "Youtube",
icon: "ph:youtube-logo-thin",
labelClass: "text-xs",
});
}
return items
}
export function formatDate( d:number ){
const { $i18n } = useNuxtApp();
const locale = $i18n.locale;
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.video) {
items.push({
to: files.video,
target: "_blank",
label: t("Activities.video"),
icon: "ph:file-video-thin",
labelClass: "text-xs",
});
}
return locationParts.filter(Boolean).join(', ');
}
export function getDay( d:number ){
const { $i18n } = useNuxtApp();
const locale = $i18n.locale;
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.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;
}
export function getThumbnail( item:ItemObject ) {
console.log("ITEM",item);
let path = item.thumbnail
if (!path) {
return "https://images.carpa.com/tr:w-900,f-auto/youtube_thumbnail_46396.png"
export function formatDate(d: number) {
if (!d) {
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;
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 `https://images.carpa.com/${item.type}/${path}?tr=w-900`
return `/${locale.value}/${item.type}/${date.year()}/${month.padStart(2, "0")}/${slug}`;
}
}
export function getAuthor( type:string ){
let author = ''
switch (type){
case "activities":
author = 'Dr. José Benjamín Pérez Matos'
break
case "conferences":
author = 'Dr. William Soto Santiago'
break
case "sermons":
author = 'William Marrion Branham';
break
}
return author
export function getThumbnail(item: ItemObject) {
console.log("ITEM", item);
let path = item.thumbnail;
if (!path) {
return "https://images.carpa.com/tr:w-900,f-auto/youtube_thumbnail_46396.png";
} else {
return `https://images.carpa.com/${item.type}/${path}?tr=w-900`;
}
}
export function getAuthor(type: string) {
let author = "";
switch (type) {
case "activities":
author = "Dr. José Benjamín Pérez Matos";
break;
case "conferences":
author = "Dr. William Soto Santiago";
break;
case "sermons":
author = "William Marrion Branham";
break;
}
return author;
}
// 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.

View File

@ -1,5 +1,5 @@
/* Copy to Clipboard */
export async function copyToClipboard(textToCopy: string, type: string) {
export async function copyToClipboard(type: string) {
const toast = useToast()
const selection = window.getSelection()
@ -29,3 +29,18 @@ export async function copyToClipboard(textToCopy: string, type: string) {
console.error('Failed to copy: ', err)
}
}
export function debugDocument(json) {
let data = ''
// Iterate through keys and values
for (const key in json) {
if (json.hasOwnProperty(key)) {
data += `${key}: ${json[key]}\n\r`
}
}
return data
}
export function addNote() {
return 'yay'
}