UI Fixes
Adding page component to document results Adding aside to handle notes and highlights in future
This commit is contained in:
parent
e2deaa8066
commit
1756aadaff
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue