Skip to content

Commit c3b2f08

Browse files
Merge pull request #56 from appwrite/feat-search
feat: search
2 parents 36d1c24 + fd0f1cd commit c3b2f08

6 files changed

Lines changed: 286 additions & 8 deletions

File tree

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"dotenv": "^16.3.1",
5353
"highlight.js": "^11.8.0",
5454
"markdown-it": "^13.0.1",
55+
"meilisearch": "^0.35.0",
5556
"motion": "^10.16.2"
5657
}
5758
}

pnpm-lock.yaml

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/components/Search.svelte

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
<script lang="ts">
2+
import { fade } from 'svelte/transition';
3+
import type { Action } from 'svelte/action';
4+
import { MeiliSearch, type Hits, type Hit } from 'meilisearch';
5+
6+
export let open: boolean = true;
7+
8+
let value: string;
9+
let container: HTMLDivElement;
10+
11+
const client = new MeiliSearch({
12+
host: 'https://search.appwrite.org',
13+
apiKey: 'd7e83e21c0daf2a471ef4c463c7872e55b91b0cd02e2d20e9c6f6f1c4cd09ed3'
14+
});
15+
const index = client.index<Props>('website');
16+
17+
type Props = {
18+
url: string;
19+
title?: string;
20+
uid?: string;
21+
meta?: Record<string, string>;
22+
page_block?: number;
23+
urls_tags?: Array<string>;
24+
h1?: string;
25+
h2?: string;
26+
h3?: string;
27+
h4?: string;
28+
h5?: string;
29+
h6?: string;
30+
p?: string;
31+
anchor?: string;
32+
};
33+
34+
let results: Hits<Props> = [];
35+
36+
async function search(value: string) {
37+
return index.search(value, {
38+
limit: 5
39+
});
40+
}
41+
42+
async function handleInput(value: string) {
43+
const response = await search(value);
44+
results = response.hits.filter((hit) => hit.h1);
45+
}
46+
47+
function handleExit(event: MouseEvent & { currentTarget: EventTarget & HTMLDivElement }) {
48+
if (event.target === container) {
49+
open = false;
50+
}
51+
}
52+
53+
function createHref(hit: Hit<Props>): string {
54+
const anchor = hit.anchor === '#' ? '' : hit.anchor ?? '';
55+
const target = new URL(hit.url + anchor);
56+
57+
return target.toString();
58+
}
59+
60+
const recommended: Hits<Props> = [
61+
{
62+
uid: 'recommended-references-account',
63+
url: 'https://website-appwrite.vercel.app/docs/references/cloud/client-web/databases',
64+
h1: 'References',
65+
h2: 'Databases'
66+
},
67+
{
68+
uid: 'recommended-references-teans',
69+
url: 'https://website-appwrite.vercel.app/docs/references/cloud/client-web/teams',
70+
h1: 'References',
71+
h2: 'Teams'
72+
},
73+
{
74+
uid: 'recommended-references-databases',
75+
url: 'https://website-appwrite.vercel.app/docs/references/cloud/client-web/databases',
76+
h1: 'References',
77+
h2: 'Databases'
78+
},
79+
{
80+
uid: 'recommended-references-storage',
81+
url: 'https://website-appwrite.vercel.app/docs/references/cloud/client-web/storage',
82+
h1: 'References',
83+
h2: 'Storage'
84+
}
85+
];
86+
87+
const elements = new Map<number, HTMLElement>();
88+
89+
const arrowKeyFocus: Action<HTMLElement, number> = (node, index) => {
90+
elements.set(index, node);
91+
const callback = (event: KeyboardEvent) => {
92+
switch (event.key) {
93+
case 'ArrowDown':
94+
{
95+
event.preventDefault();
96+
const target = index + 1;
97+
if (elements.has(target)) {
98+
elements.get(target)?.focus();
99+
}
100+
}
101+
break;
102+
case 'ArrowUp':
103+
{
104+
event.preventDefault();
105+
const target = index - 1;
106+
if (elements.has(target)) {
107+
elements.get(target)?.focus();
108+
}
109+
}
110+
break;
111+
}
112+
};
113+
114+
node.addEventListener('keydown', callback);
115+
116+
return {
117+
destroy() {
118+
elements.delete(index);
119+
node.removeEventListener('keydown', callback);
120+
}
121+
};
122+
};
123+
124+
$: value && handleInput(value);
125+
</script>
126+
127+
<!-- svelte-ignore a11y-no-static-element-interactions -->
128+
<!-- svelte-ignore a11y-click-events-have-key-events -->
129+
{#if open}
130+
<div
131+
class="u-position-fixed u-padding-0 u-inset-0 u-flex u-main-center u-cross-center"
132+
style:z-index="100"
133+
style:background="hsl(var(--aw-color-black) / 0.3)"
134+
style:backdrop-filter="blur(15px)"
135+
style:-webkit-backdrop-filter="blur(15px)"
136+
bind:this={container}
137+
on:click={handleExit}
138+
transition:fade={{ duration: 50 }}
139+
>
140+
<div
141+
class="aw-input-text-search-wrapper aw-u-max-width-680 aw-u-margin-inline-20 u-width-full-line"
142+
>
143+
<span class="icon-search u-z-index-5" aria-hidden="true" style="" />
144+
<div id="searchbox" />
145+
146+
<!-- svelte-ignore a11y-autofocus -->
147+
<input
148+
autofocus
149+
class="aw-input-button -u-padding-block-0 u-position-relative u-z-index-1"
150+
type="text"
151+
id="search"
152+
bind:value
153+
placeholder="Search"
154+
style="border-end-start-radius:0; border-end-end-radius:0;"
155+
use:arrowKeyFocus={-1}
156+
/>
157+
<div
158+
class="aw-card is-normal u-overflow-y-auto"
159+
style="--card-padding-mobile:1rem; border-radius:0 0 0.5rem 0.5rem; margin-block-start:-0.0625rem; max-block-size: min(18.75rem, calc(100vh - 5.5rem)); border-block-start-width:0;"
160+
>
161+
<div class="u-flex-vertical u-gap-24">
162+
{#if value}
163+
{#if results.length > 0}
164+
<section>
165+
<h6 class="aw-eyebrow">{results.length} results found</h6>
166+
<ul class="u-margin-block-start-8">
167+
{#each results as hit, i (hit.uid)}
168+
<li>
169+
<a
170+
href={createHref(hit)}
171+
use:arrowKeyFocus={i}
172+
class="aw-button aw-caption-400 is-text u-flex-vertical u-gap-8 u-min-width-100-percent aw-u-padding-block-4 aw-u-cross-start u-max-width-100-percent"
173+
>
174+
<div class="aw-u-trim-1">
175+
<span class="aw-u-color-text-secondary">{hit.h1}</span>
176+
{#if hit.h2}
177+
<span class="aw-u-color-text-secondary"> / </span>
178+
<span class="aw-u-color-text-primary">{hit.h2}</span>
179+
{/if}
180+
</div>
181+
{#if hit.p}
182+
<div class="u-inline aw-u-color-text-secondary aw-u-trim-1">
183+
{hit.p}
184+
</div>
185+
{/if}
186+
</a>
187+
</li>
188+
{/each}
189+
</ul>
190+
</section>
191+
{:else}
192+
<p class="aw-caption-400">No results found for <span class="u-bold">{value}</span></p>
193+
{/if}
194+
{/if}
195+
<section>
196+
<h6 class="aw-eyebrow">Recommended</h6>
197+
<ul class="u-margin-block-start-8">
198+
{#key results.length}
199+
{#each recommended as hit, i (hit.uid)}
200+
{@const index = i + (results.length ? results.length : 0)}
201+
<li>
202+
<a
203+
href={createHref(hit)}
204+
use:arrowKeyFocus={index}
205+
class="aw-button aw-caption-400 is-text u-flex-vertical u-gap-8 u-min-width-100-percent aw-u-padding-block-4 aw-u-cross-start"
206+
>
207+
<div class="aw-u-trim-1">
208+
<span class="aw-u-color-text-secondary">{hit.h1}</span>
209+
{#if hit.h2}
210+
<span class="aw-u-color-text-secondary"> / </span>
211+
<span class="aw-u-color-text-primary">{hit.h2}</span>
212+
{/if}
213+
</div>
214+
</a>
215+
</li>
216+
{/each}
217+
{/key}
218+
</ul>
219+
</section>
220+
</div>
221+
</div>
222+
</div>
223+
</div>
224+
{/if}

src/lib/layouts/Docs.svelte

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
export type DocsLayoutState = {
77
showReferences: boolean;
88
showSidenav: boolean;
9+
showSearch: boolean;
910
currentVariant: DocsLayoutVariant | null;
1011
};
1112
export const layoutState = writable<DocsLayoutState>({
1213
showReferences: false,
1314
showSidenav: false,
15+
showSearch: false,
1416
currentVariant: null
1517
});
1618
export function toggleReferences() {
@@ -30,6 +32,8 @@
3032
</script>
3133

3234
<script lang="ts">
35+
import Search from '$lib/components/Search.svelte';
36+
3337
export let variant: DocsLayoutVariant = 'default';
3438
3539
const variantClasses: Record<DocsLayoutVariant, string> = {
@@ -117,7 +121,10 @@
117121
</ul>
118122
</nav>
119123
<div class="u-flex u-stretch aw-u-margin-inline-start-48">
120-
<button class="aw-input-button aw-u-flex-basis-400">
124+
<button
125+
class="aw-input-button aw-u-flex-basis-400"
126+
on:click={() => ($layoutState.showSearch = true)}
127+
>
121128
<span class="icon-search" aria-hidden="true" />
122129
<span class="text">Search in docs</span>
123130

@@ -165,3 +172,5 @@
165172
<slot />
166173
</div>
167174
</div>
175+
176+
<Search bind:open={$layoutState.showSearch} />

src/lib/layouts/Sidebar.svelte

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@
3838
class:is-transparent={$layoutState.currentVariant !== 'two-side-navs'}
3939
>
4040
<div class="aw-side-nav-wrapper">
41-
<button class="aw-input-text aw-is-not-desktop">
41+
<button
42+
class="aw-input-text aw-is-not-desktop"
43+
on:click={() => ($layoutState.showSearch = true)}
44+
>
4245
<span class="icon-search" />
4346
<span class="text">Search in docs</span>
4447
</button>

src/markdoc/nodes/Link.svelte

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,4 @@
77
const rel = isExternal ? 'noopener nofollow' : undefined;
88
</script>
99

10-
<a class="aw-link" {href} {title} {target} {rel}>
11-
<slot />
12-
{#if !isExternal}
13-
<!-- <span class="icon-cheveron-right" /> -->
14-
{/if}
15-
</a>
10+
<a class="aw-link" {href} {title} {target} {rel}><slot /></a>

0 commit comments

Comments
 (0)