feat(docs): search

This commit is contained in:
LukaDev 2024-09-29 17:49:37 +02:00
parent ba0345d58a
commit 0758dac966
4 changed files with 251 additions and 55 deletions

Binary file not shown.

View file

@ -1,57 +1,57 @@
{
"name": "website",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-vercel": "^5.4.3",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/typography": "^0.5.14",
"@types/eslint": "^9.6.0",
"@types/gunzip-maybe": "^1.4.2",
"@types/tar-stream": "^3.1.3",
"autoprefixer": "^10.4.20",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.36.0",
"globals": "^15.0.0",
"mdsvex": "^0.11.2",
"prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"svelte": "^5.0.0-next.1",
"svelte-check": "^3.6.0",
"tailwindcss": "^3.4.9",
"typescript": "^5.0.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.0.3"
},
"type": "module",
"dependencies": {
"@fontsource-variable/nunito-sans": "^5.0.14",
"@shikijs/rehype": "^1.13.0",
"bits-ui": "^0.21.13",
"date-fns": "^3.6.0",
"gunzip-maybe": "^1.4.2",
"lucide-svelte": "^0.427.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.0",
"remark-gemoji": "^8.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"shiki": "^1.13.0",
"tar-stream": "^3.1.7",
"unified": "^11.0.5"
}
"name": "website",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --check . && eslint .",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-vercel": "^5.4.4",
"@sveltejs/kit": "^2.6.1",
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.7",
"@tailwindcss/typography": "^0.5.15",
"@types/eslint": "^9.6.1",
"@types/gunzip-maybe": "^1.4.2",
"@types/tar-stream": "^3.1.3",
"autoprefixer": "^10.4.20",
"eslint": "^9.11.1",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.44.1",
"globals": "^15.9.0",
"mdsvex": "^0.12.3",
"prettier": "^3.3.3",
"prettier-plugin-svelte": "^3.2.7",
"prettier-plugin-tailwindcss": "^0.6.8",
"svelte": "^5.0.0-next.260",
"svelte-check": "^4.0.4",
"tailwindcss": "^3.4.13",
"typescript": "^5.6.2",
"typescript-eslint": "^8.7.0",
"vite": "^5.4.8"
},
"type": "module",
"dependencies": {
"@fontsource-variable/nunito-sans": "^5.1.0",
"@shikijs/rehype": "^1.21.0",
"bits-ui": "^0.21.15",
"date-fns": "^4.1.0",
"gunzip-maybe": "^1.4.2",
"lucide-svelte": "^0.446.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"rehype-stringify": "^10.0.1",
"remark-gemoji": "^8.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"shiki": "^1.21.0",
"tar-stream": "^3.1.7",
"unified": "^11.0.5"
}
}

View file

@ -0,0 +1,170 @@
<script lang="ts">
import { goto } from "$app/navigation"
import { TARGET_KIND_DISPLAY_NAMES } from "$lib/registry-api.js"
import { Pagination } from "bits-ui"
import { formatDistanceToNow } from "date-fns"
import { ChevronLeft, ChevronRight, SearchX } from "lucide-svelte"
const { data } = $props()
let displayDates = $state(false)
$effect(() => {
displayDates = true
})
console.log(data.result)
</script>
<div class="mx-auto max-w-screen-lg px-4">
{#await data.result}
<div class="sr-only">Loading...</div>
<div
class="my-8 flex flex-col sm:flex-row sm:items-center sm:justify-between"
aria-hidden="true"
>
<div class="bg-card-hover w-20 animate-pulse rounded text-transparent">...</div>
<div class="bg-card-hover mx-auto mt-8 h-8 w-48 animate-pulse rounded sm:m-0"></div>
</div>
<div class="mb-8 space-y-4">
{#each Array.from({ length: 10 }).map((_, i) => i) as i}
<div class="bg-card/50 overflow-hidden rounded px-5 py-4">
<div class="mb-1 flex items-center justify-between">
<div class="bg-card-hover w-52 animate-pulse rounded text-xl text-transparent">...</div>
<div
class="bg-card-hover hidden w-32 animate-pulse rounded text-sm text-transparent sm:block"
>
...
</div>
</div>
<div
class="bg-card-hover mb-3 w-96 max-w-full animate-pulse rounded text-sm text-transparent"
>
...
</div>
<div class="bg-card-hover w-16 max-w-full animate-pulse rounded text-sm text-transparent">
...
</div>
</div>
{/each}
</div>
<div class="bg-card-hover mx-auto my-8 h-8 w-48 animate-pulse rounded"></div>
{:then result}
{#if result.data.length === 0}
<div class="flex flex-col items-center py-32 text-center">
<SearchX class="mb-6 size-16" />
<h1 class="text-heading font-bold">No results</h1>
<p class="text-balance">We didn't find any packages matching your search query.</p>
</div>
{:else}
{#snippet pagination()}
<Pagination.Root
count={result.count}
page={data.page}
perPage={data.pageSize}
onPageChange={(page) => {
const params = new URLSearchParams()
params.set("q", data.query)
params.set("page", page.toString())
goto(`/search?${params}`)
}}
let:pages
>
<div class="flex items-center space-x-1">
<Pagination.PrevButton
class="hover:enabled:bg-card-hover inline-flex size-8 items-center justify-center rounded transition disabled:opacity-50"
>
<ChevronLeft />
</Pagination.PrevButton>
{#each pages as page (page.key)}
{#if page.type === "ellipsis"}
<div class="px-2">...</div>
{:else}
<Pagination.Page
{page}
class="hover:bg-card-hover hover:text-heading data-[selected]:bg-primary-bg data-[selected]:text-primary-fg inline-flex size-8 items-center justify-center rounded font-bold transition"
>
{page.value}
</Pagination.Page>
{/if}
{/each}
<Pagination.NextButton
class="hover:enabled:bg-card-hover inline-flex size-8 items-center justify-center rounded transition disabled:opacity-50"
>
<ChevronRight />
</Pagination.NextButton>
</div>
</Pagination.Root>
{/snippet}
<div class="my-8 flex flex-col sm:flex-row sm:items-center sm:justify-between">
<h1 class="font-bold">{result.count} {result.count > 1 ? "results" : "result"}</h1>
<div class="mx-auto mt-8 sm:m-0">
{@render pagination()}
</div>
</div>
<div class="mb-8 space-y-4">
{#each result.data as pkg}
{@const [scope, name] = pkg.name.split("/")}
{#snippet timeAndVersion()}
<time datetime={pkg.published_at}>
{#if displayDates}
{formatDistanceToNow(new Date(pkg.published_at), { addSuffix: true })}
{:else}
...
{/if}
</time>
<span>{" · "}</span>
<span class="truncate">v{pkg.version}</span>
{/snippet}
<article
class="bg-card hover:bg-card-hover relative overflow-hidden rounded px-5 py-4 transition"
>
<div class="mb-1 flex items-center justify-between">
<h3 class="truncate text-xl font-semibold">
<a
href={`/packages/${pkg.name}`}
class="after:absolute after:inset-0 after:content-['']"
>
<span class="text-heading">{scope}/</span><span class="text-light">{name}</span>
</a>
</h3>
<div
class="text-heading hidden text-sm font-semibold sm:block"
class:invisible={!displayDates}
>
{@render timeAndVersion()}
</div>
</div>
<p class="mb-3 h-[1lh] overflow-hidden truncate text-sm">{pkg.description}</p>
<div
class={`text-primary text-sm font-bold ${displayDates ? "" : "invisible sm:visible"}`}
>
{pkg.targets.map((target) => TARGET_KIND_DISPLAY_NAMES[target.kind]).join(", ")}
<span class="sm:hidden">
<span>{" · "}</span>
{@render timeAndVersion()}
</span>
</div>
</article>
{/each}
</div>
<div class="mx-auto my-8 max-w-min">
{@render pagination()}
</div>
{/if}
{:catch error}
<div class="mx-auto max-w-screen-xl px-4 py-32 text-center">
<h1 class="text-heading mb-1 text-4xl font-bold">Error</h1>
<p class="text-lg">{error.message}</p>
</div>
{/await}
</div>

View file

@ -0,0 +1,26 @@
import { fetchRegistryJson, type SearchResponse } from "$lib/registry-api"
import type { PageLoad } from "./$types"
const PAGE_SIZE = 50
export const load: PageLoad = async ({ fetch, url }) => {
const query = url.searchParams.get("q") ?? ""
let page = parseInt(url.searchParams.get("page") ?? "1")
if (isNaN(page) || page < 1) {
page = 1
}
const params = new URLSearchParams()
params.set("query", query)
params.set("offset", String((page - 1) * PAGE_SIZE))
const result = fetchRegistryJson<SearchResponse>(`search?${params}`, fetch)
return {
query,
page,
pageSize: PAGE_SIZE,
result,
}
}