From 1b5483602f8eb2b28698f7c848a5205140dec4e7 Mon Sep 17 00:00:00 2001 From: Jonas Braathen Date: Mon, 25 May 2026 23:39:11 +0200 Subject: [PATCH] web: highlight word matches, limit search results --- web/src/app/sok/page.tsx | 14 ++++- web/src/components/search/SearchResults.tsx | 52 ++++++++++++++++--- web/src/components/search/SearchShell.tsx | 10 +++- .../search/searchContainer.module.scss | 17 ++++++ 4 files changed, 82 insertions(+), 11 deletions(-) diff --git a/web/src/app/sok/page.tsx b/web/src/app/sok/page.tsx index 73d7b07..2405a09 100644 --- a/web/src/app/sok/page.tsx +++ b/web/src/app/sok/page.tsx @@ -17,6 +17,8 @@ export default async function Page({ }) { const { q: query } = (await searchParams) ?? {}; let results: SearchResult[] = []; + let totalCount = 0; + const RESULT_LIMIT = 500; if (query) { const searchQuery = graphql(` @@ -64,13 +66,21 @@ export default async function Page({ `); const { data } = await getClient().query(searchQuery, { query }); - results = (data?.results ?? []) as SearchResult[]; + const all = (data?.results ?? []) as SearchResult[]; + totalCount = all.length; + results = all.slice(0, RESULT_LIMIT); } return (
- {query ? : null} + {query ? ( + + ) : null}
); diff --git a/web/src/components/search/SearchResults.tsx b/web/src/components/search/SearchResults.tsx index 380f776..ef44a74 100644 --- a/web/src/components/search/SearchResults.tsx +++ b/web/src/components/search/SearchResults.tsx @@ -76,28 +76,64 @@ function getResultSnippet(result: SupportedResult): string | null { } } -export function SearchResults({ results }: { results: SearchResult[] }) { +function highlight(text: string, query: string): React.ReactNode { + if (query.length < 2) return text; + const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(escaped, "gi"); + const nodes: React.ReactNode[] = []; + let lastIndex = 0; + for (const match of text.matchAll(pattern)) { + if (match.index > lastIndex) { + nodes.push(text.slice(lastIndex, match.index)); + } + nodes.push({match[0]}); + lastIndex = match.index + match[0].length; + } + if (lastIndex < text.length) { + nodes.push(text.slice(lastIndex)); + } + return nodes; +} + +export function SearchResults({ + results, + totalCount, + query, +}: { + results: SearchResult[]; + totalCount: number; + query: string; +}) { if (!results.length) { return (
- Ingen resultater +

Ingen treff på «{query}»

); } const supportedResults = results.filter(isSupported); + const truncated = totalCount > results.length; return (

- {results.length} resultater + {truncated + ? `Viser de første ${results.length} av ${totalCount} treff — prøv et mer spesifikt søk.` + : `${results.length} resultater`}

{supportedResults.map((result) => ( - + ))}
); } -function ResultRow({ result }: { result: SupportedResult }) { +function ResultRow({ + result, + query, +}: { + result: SupportedResult; + query: string; +}) { const image = getResultImage(result); const snippet = getResultSnippet(result); const date = getResultDate(result); @@ -108,9 +144,11 @@ function ResultRow({ result }: { result: SupportedResult }) {
{resultType} -

{result.title}

+

{highlight(result.title, query)}

{date &&

{date}

} - {snippet &&

{snippet}

} + {snippet && ( +

{highlight(snippet, query)}

+ )}
{image?.url && (
diff --git a/web/src/components/search/SearchShell.tsx b/web/src/components/search/SearchShell.tsx index dad6e4d..b59ea01 100644 --- a/web/src/components/search/SearchShell.tsx +++ b/web/src/components/search/SearchShell.tsx @@ -22,8 +22,9 @@ export function SearchShell({ }) { const { replace } = useRouter(); const [inputValue, setInputValue] = useState(initialQuery); - const [, startTransition] = useTransition(); + const [isPending, startTransition] = useTransition(); const lastPushedRef = useRef(initialQuery); + const fetching = isPending || inputValue !== lastPushedRef.current; const pushQuery = useDebouncedCallback((next: string) => { lastPushedRef.current = next; @@ -73,7 +74,12 @@ export function SearchShell({
- {children} +
+ {children} +
); } diff --git a/web/src/components/search/searchContainer.module.scss b/web/src/components/search/searchContainer.module.scss index 810a225..5e76175 100644 --- a/web/src/components/search/searchContainer.module.scss +++ b/web/src/components/search/searchContainer.module.scss @@ -5,6 +5,13 @@ a { text-decoration: none; } + + mark { + background: var(--color-goldenBeige); + color: inherit; + padding: 0 .05em; + border-radius: 2px; + } } .searchField { @@ -51,6 +58,16 @@ .snippet { margin-top: 0.25em; + display: -webkit-box; + -webkit-line-clamp: 3; + line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.fetching { + opacity: 0.5; + transition: opacity .15s ease; } .thumb {