mirror of
https://github.com/pesde-pkg/pesde.git
synced 2025-05-04 10:33:47 +01:00
feat(docs): search
This commit is contained in:
parent
ba0345d58a
commit
0758dac966
4 changed files with 251 additions and 55 deletions
Binary file not shown.
|
@ -12,45 +12,45 @@
|
|||
"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",
|
||||
"@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.0.0",
|
||||
"eslint": "^9.11.1",
|
||||
"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"
|
||||
"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.0.14",
|
||||
"@shikijs/rehype": "^1.13.0",
|
||||
"bits-ui": "^0.21.13",
|
||||
"date-fns": "^3.6.0",
|
||||
"@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.427.0",
|
||||
"lucide-svelte": "^0.446.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"rehype-sanitize": "^6.0.0",
|
||||
"rehype-stringify": "^10.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.0",
|
||||
"shiki": "^1.13.0",
|
||||
"remark-rehype": "^11.1.1",
|
||||
"shiki": "^1.21.0",
|
||||
"tar-stream": "^3.1.7",
|
||||
"unified": "^11.0.5"
|
||||
}
|
||||
|
|
170
website/src/routes/search/+page.svelte
Normal file
170
website/src/routes/search/+page.svelte
Normal 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>
|
26
website/src/routes/search/+page.ts
Normal file
26
website/src/routes/search/+page.ts
Normal 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,
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue