diff --git a/dist/static/style.css b/dist/static/style.css index c6ab82d..bcbd249 100644 --- a/dist/static/style.css +++ b/dist/static/style.css @@ -4,9 +4,15 @@ --color-background: oklch(1 0 0); --color-base: oklch(0 0 0); + --color-contrast-bg: oklch(0 0 0); + --color-contrast-fg: oklch(1 0 0); --border-size: 4px; } +* { + box-sizing: border-box; +} + html, body { font-family: var(--font-family-base); width: 100%; @@ -30,12 +36,41 @@ input[type="search"] { } } +.form-main { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} +button { + padding: .75rem 0.5rem; + font-family: var(--font-family-head); + background: var(--color-contrast-bg); + color: var(--color-contrast-fg); + border: 2px solid var(--color-contrast-bg); + box-shadow: 0 0 0 4px var(--color-contrast-bg); + + &:hover, + &:focus, + &:active { + border: 2px solid var(--color-contrast-fg); + } +} + h1, h2, h3, h4, h5 { font-family: var(--font-family-head); - margin: 2em 0 1em; + margin: 3em 0 1em; padding: 0; } +h1 { + margin: 1em 0; +} + +p { + margin: 1em 0; +} + ul { margin: 1rem 0; padding: 0; @@ -62,3 +97,12 @@ blockquote { max-width: 100%; word-break: break-all; } + +.result-item-first-level { + & .result-item-first-level__sublist { + display: none; + } + & label:has(.result-item-first-level__checkbox:checked) ~ .result-item-first-level__sublist { + display: block; + } +} diff --git a/functions/index.js b/functions/index.js index 913d514..0b4c8c1 100644 --- a/functions/index.js +++ b/functions/index.js @@ -15,26 +15,28 @@ export const onRequestGet = async (context) => { docsPages, ] = await stats(env); const doneIn = Date.now() - startTime; - const hasResults = result?.hits?.length > 0; - - const hits = {}; - - result?.hits?.forEach((hit) => { - if (!hits[hit.index]) { - hits[hit.index] = { - label: hit.index === 'blogs' ? 'Blogs' : 'Docs', - items: [], - }; - } - - }); - + const hasBlogs = result?.hits.blogs.length > 0; + const hasDocs = result?.hits.docs.length > 0; + const results = []; + if (result.hits.blogs.length) { + results.push({ + name: 'Blogs', + hits: result.hits.blogs, + }); + } + if (result.hits.docs.length) { + results.push({ + name: 'Docs', + hits: result.hits.docs, + }); + } const view = { q, title: 'kukei.eu', - hits, - hasResults, - noResults: q && !hasResults, + results, + hasQuery: !!q, + noResults: !(hasBlogs || hasDocs), + hasResults: results.length > 0, doneIn, hash, blogPages, diff --git a/functions/template.html b/functions/template.html index c2de52d..5ae0c6f 100644 --- a/functions/template.html +++ b/functions/template.html @@ -14,40 +14,54 @@

Kukey.eu
curated search for web developers

So far indexed {{ blogPages }} pages of blogs and {{ docsPages }} pages of docs.

- +
+ + +
- {{#hasResults}} + {{#hasQuery}} +

Results

Search results generated in {{ doneIn }}ms

-
-

Blogs

- -
-
-

Docs

- -
- {{/hasResults}} - {{#noResults}} -

No results found

- {{/noResults}} + {{#hasResults}} + {{#results}} +
+

{{ name }}

+ +
+ {{/results}} + {{/hasResults}} + {{#noResults}} +

No results found

+ {{/noResults}} + {{/hasQuery}} + diff --git a/lib/search.js b/lib/search.js index 4bfec95..fba0861 100644 --- a/lib/search.js +++ b/lib/search.js @@ -10,44 +10,120 @@ const getMeiliClient = (env) => { const makeOptions = (p) => ({ attributesToHighlight: ['content'], attributesToCrop: ['content'], - facets: ['hostname'], + facets: ['hostname', 'lang'], cropLength: 50, limit: 10, offset: p, }); -const doSearch = async (result, env, index, q, p) => { +/** + * @typedef {Object} ResultItem + * @property {string} url + * @property {string} highlight + * @property {string} index + * @property {string} excerpt + * @property {string} title + * @property {string} lang + * @property {string} hostname + */ +/** + * @typedef {Object} ResultGrouped + * @extends {ResultItem} + * @property {ResultItem[]} subItems + */ + +/** + * + * @param results + * @param env + * @param index + * @param q + * @param p + * @returns {Promise} + */ +const doSearch = async (results, env, index, q, p) => { const meiliClient = getMeiliClient(env); - const results = await meiliClient.index(index).search(q, makeOptions(p)); + const searchResult = await meiliClient.index(index).search(q, makeOptions(p)); const { facetDistribution, hits - } = results; + } = searchResult; - result.facets.push(...facetDistribution); + Object.keys(facetDistribution).forEach((key) => { + const facetData = facetDistribution[key]; + if (!results.facets[key]) { + results.facets[key] = []; + } + Object.keys(facetData).forEach((facetKey) => { + results.facets[key].push({ + count: facetData[facetKey], + value: facetKey, + }); + }); + }); + /** + * + * @typedef {Map} + */ + const originsMap = new Map(); hits.forEach((el) => { const highlightRaw = el._formatted?.content ?? ''; // remove excessive whitespace const highlight = highlightRaw.replace(/\s+/g, ' '); - result.hits.push({ + const final = { url: el.url, highlight, index, excerpt: el.excerpt, title: el.title, - }); + hostname: el.hostname, + }; + + const indexArr = results.hits[final.index]; + let existingOrigin = originsMap.get(el.hostname); + + if (existingOrigin) { + existingOrigin.subItems.push(final); + return; + } + + final.subItems = []; + originsMap.set(el.hostname, final); + indexArr.push(final); }); }; + +/** + * @typedef {Object} Facet + * @property {string} name + * @property {Array<{count: number, value: string}>} data + */ +/** + * @typedef {Object} SearchResult + * @property {Array} facets + * @property {Object>} hits + */ +/** + * + * @param {Object} env + * @param {string} q + * @param {number} p + * @returns {Promise<{SearchResult}>} + */ export const search = async (env, q, p = 0) => { const result = { facets: [], - hists: [], + hits: { + 'blogs': [], + 'docs': [], + }, }; - await doSearch(env, 'blogs', q, p); - await doSearch(env, 'docs', q, p); + + await doSearch(result, env, 'blogs', q, p); + await doSearch(result, env, 'docs', q, p); return result; };