From 0f06ce56ce21faf3789272e988aa7684b9fda394 Mon Sep 17 00:00:00 2001 From: Wayne <5291640+ringoinca@users.noreply.github.com> Date: Mon, 21 Jul 2025 16:28:50 +0300 Subject: [PATCH] Search page UI: Modify highlights, loading skeleton --- .../backend/src/services/SearchService.ts | 2 +- .../src/lib/components/ui/skeleton/index.ts | 7 + .../components/ui/skeleton/skeleton.svelte | 17 ++ .../routes/dashboard/search/+page.server.ts | 63 +++--- .../src/routes/dashboard/search/+page.svelte | 203 ++++++++++-------- 5 files changed, 166 insertions(+), 126 deletions(-) create mode 100644 packages/frontend/src/lib/components/ui/skeleton/index.ts create mode 100644 packages/frontend/src/lib/components/ui/skeleton/skeleton.svelte diff --git a/packages/backend/src/services/SearchService.ts b/packages/backend/src/services/SearchService.ts index 1a9f92c..a4ac4d5 100644 --- a/packages/backend/src/services/SearchService.ts +++ b/packages/backend/src/services/SearchService.ts @@ -40,7 +40,7 @@ export class SearchService { const searchParams: SearchParams = { limit, offset: (page - 1) * limit, - attributesToHighlight: ['body', 'attachments.*.content', 'from', 'to', 'subject', 'cc', 'bcc'], + attributesToHighlight: ["*"], showMatchesPosition: true, sort: ['timestamp:desc'] }; diff --git a/packages/frontend/src/lib/components/ui/skeleton/index.ts b/packages/frontend/src/lib/components/ui/skeleton/index.ts new file mode 100644 index 0000000..186db21 --- /dev/null +++ b/packages/frontend/src/lib/components/ui/skeleton/index.ts @@ -0,0 +1,7 @@ +import Root from "./skeleton.svelte"; + +export { + Root, + // + Root as Skeleton, +}; diff --git a/packages/frontend/src/lib/components/ui/skeleton/skeleton.svelte b/packages/frontend/src/lib/components/ui/skeleton/skeleton.svelte new file mode 100644 index 0000000..c7e3d26 --- /dev/null +++ b/packages/frontend/src/lib/components/ui/skeleton/skeleton.svelte @@ -0,0 +1,17 @@ + + +
diff --git a/packages/frontend/src/routes/dashboard/search/+page.server.ts b/packages/frontend/src/routes/dashboard/search/+page.server.ts index bf7ba09..e033506 100644 --- a/packages/frontend/src/routes/dashboard/search/+page.server.ts +++ b/packages/frontend/src/routes/dashboard/search/+page.server.ts @@ -1,42 +1,43 @@ -import type { PageServerLoad, Actions } from './$types'; +import type { PageServerLoad, Actions, RequestEvent } from './$types'; import { api } from '$lib/server/api'; import type { SearchResult } from '@open-archiver/types'; -export const load: PageServerLoad = async () => { - return { - searchResult: null, - query: '' - }; +async function performSearch(query: string, event: RequestEvent) { + if (!query) { + return { searchResult: null, query: '' }; + } + + try { + const response = await api('/search', event, { + method: 'POST', + body: JSON.stringify({ query }) + }); + + if (!response.ok) { + const error = await response.json(); + return { searchResult: null, query, error: error.message }; + } + + const searchResult = (await response.json()) as SearchResult; + return { searchResult, query }; + } catch (error) { + return { + searchResult: null, + query, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } +} + +export const load: PageServerLoad = async (event) => { + const query = event.url.searchParams.get('query') || ''; + return performSearch(query, event); }; export const actions: Actions = { default: async (event) => { const formData = await event.request.formData(); const query = formData.get('query') as string; - - if (!query) { - return { searchResult: null, query: '' }; - } - - try { - const response = await api( - '/search', - event, - { - method: 'POST', - body: JSON.stringify({ query }) - } - ); - - if (!response.ok) { - const error = await response.json(); - return { searchResult: null, query, error: error.message }; - } - - const searchResult = await response.json() as SearchResult; - return { searchResult, query }; - } catch (error) { - return { searchResult: null, query, error: error instanceof Error ? error.message : 'Unknown error' }; - } + return performSearch(query, event); } }; diff --git a/packages/frontend/src/routes/dashboard/search/+page.svelte b/packages/frontend/src/routes/dashboard/search/+page.svelte index 73cd7d8..7bb4e09 100644 --- a/packages/frontend/src/routes/dashboard/search/+page.svelte +++ b/packages/frontend/src/routes/dashboard/search/+page.svelte @@ -9,93 +9,84 @@ CardTitle, CardDescription } from '$lib/components/ui/card'; - import type { SearchResult } from '@open-archiver/types'; + import { onMount } from 'svelte'; + import { Skeleton } from '$lib/components/ui/skeleton'; let { data, form }: { data: PageData; form: ActionData } = $props(); const searchResult = form?.searchResult ?? data.searchResult; - console.log(searchResult); const query = form?.query ?? data.query; const error = form?.error; - function escapeHTML(text: string) { - if (!text) return ''; - return text - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace('', ''); + let isMounted = $state(false); + onMount(() => { + isMounted = true; + }); + + function shadowRender(node: HTMLElement, html: string | undefined) { + if (html === undefined) return; + + const shadow = node.attachShadow({ mode: 'open' }); + const style = document.createElement('style'); + style.textContent = `em { background-color: #fde047; font-style: normal; color: #1f2937; }`; // yellow-300, gray-800 + shadow.appendChild(style); + const content = document.createElement('div'); + content.innerHTML = html; + shadow.appendChild(content); + + return { + update(newHtml: string | undefined) { + if (newHtml === undefined) return; + content.innerHTML = newHtml; + } + }; } - function getHighlightedHTMLFormatted(formatted: SearchResult['hits']) {} - - function getHighlightedHTML( - text: string, - positions: { start: number; length: number }[] - ): string { - if (!text || !positions) { - return text; - } - - // sort positions by start index - positions.sort((a, b) => a.start - b.start); - - let highlighted = ''; - let lastIndex = 0; - positions.forEach(({ start, length }) => { - highlighted += escapeHTML(text.substring(lastIndex, start)); - highlighted += `${escapeHTML( - text.substring(start, start + length) - )}`; - lastIndex = start + length; - }); - highlighted += escapeHTML(text.substring(lastIndex)); - return highlighted; - } - - function getSnippets( - text: string, - positions: { start: number; length: number }[], - contextLength = 50 - ) { - if (!text || !positions) { + function getHighlightedSnippets(text: string | undefined, snippetLength = 80): string[] { + if (!text || !text.includes('')) { return []; } - // sort positions by start index - positions.sort((a, b) => a.start - b.start); - const snippets: string[] = []; - let lastEnd = -1; + const regex = /.*?<\/em>/g; + let match; + let lastIndex = 0; - for (const { start, length } of positions) { - if (start < lastEnd) { - // Skip overlapping matches to avoid duplicate snippets + while ((match = regex.exec(text)) !== null) { + if (match.index < lastIndex) { continue; } - const snippetStart = Math.max(0, start - contextLength); - const snippetEnd = Math.min(text.length, start + length + contextLength); - lastEnd = snippetEnd; + const matchIndex = match.index; + const matchLength = match[0].length; - let snippet = text.substring(snippetStart, snippetEnd); + const start = Math.max(0, matchIndex - snippetLength); + const end = Math.min(text.length, matchIndex + matchLength + snippetLength); - // Adjust positions to be relative to the snippet - const relativeStart = start - snippetStart; - const relativePositions = [{ start: relativeStart, length }]; + lastIndex = end; - let highlightedSnippet = getHighlightedHTML(snippet, relativePositions); + let snippet = text.substring(start, end); - if (snippetStart > 0) { - highlightedSnippet = '...' + highlightedSnippet; - } - if (snippetEnd < text.length) { - highlightedSnippet += '...'; + // Then, balance them + const openCount = (snippet.match(//g) || []).length; + + if (openCount > closeCount) { + snippet += ''; } - snippets.push(highlightedSnippet); + if (closeCount > openCount) { + snippet = '' + snippet; + } + + // Finally, add ellipsis + if (start > 0) { + snippet = '...' + snippet; + } + if (end < text.length) { + snippet += '...'; + } + + snippets.push(snippet); } return snippets; @@ -136,51 +127,75 @@