|
19 | 19 | let chosenSites: string[] = ['facebook.com']; |
20 | 20 | let similarSites: ScoredSite[] = []; |
21 | 21 |
|
22 | | - let errorMessage = false; |
| 22 | + type ValidationState = 'idle' | 'validating url' | 'loading similar' | 'error'; |
| 23 | + let validation: ValidationState = 'idle'; |
23 | 24 |
|
24 | 25 | $: { |
25 | | - api |
26 | | - .webgraphHostSimilar({ sites: chosenSites, topN: limit }) |
27 | | - .data.then((res) => (similarSites = res)); |
| 26 | + validation = 'loading similar'; |
| 27 | + api.webgraphHostSimilar({ sites: chosenSites, topN: limit }).data.then(async (res) => { |
| 28 | + await wait(1000); |
| 29 | + similarSites = res; |
| 30 | + validation = 'idle'; |
| 31 | + }); |
28 | 32 | } |
29 | 33 |
|
| 34 | + const wait = (ms: number) => new Promise((res) => setTimeout(res, ms)); |
| 35 | +
|
30 | 36 | const removeWebsite = async (site: string) => { |
31 | 37 | if (chosenSites.includes(site)) { |
32 | 38 | chosenSites = chosenSites.filter((s) => s != site); |
33 | 39 | } |
34 | 40 | }; |
35 | | - const addWebsite = async (site: string, clear = false) => { |
36 | | - errorMessage = false; |
| 41 | + const addWebsite = async ( |
| 42 | + site: string, |
| 43 | + { clear = false, validate = true }: { clear?: boolean; validate?: boolean } = {}, |
| 44 | + ) => { |
| 45 | + validation = 'idle'; |
37 | 46 | site = site.trim(); |
38 | 47 | if (!site) return; |
| 48 | + if (!validate) { |
| 49 | + chosenSites = [...chosenSites, site]; |
| 50 | + return; |
| 51 | + } |
| 52 | + validation = 'validating url'; |
39 | 53 |
|
| 54 | + await wait(1000); |
40 | 55 | const result = await api.webgraphHostKnows({ site }).data; |
41 | 56 | match(result) |
42 | 57 | .with({ type: 'unknown' }, () => { |
43 | | - errorMessage = true; |
| 58 | + validation = 'error'; |
44 | 59 | }) |
45 | 60 | .with({ type: 'known' }, async ({ site }) => { |
| 61 | + validation = 'idle'; |
46 | 62 | if (clear) inputWebsite = ''; |
47 | 63 | if (!chosenSites.includes(site)) chosenSites = [...chosenSites, site]; |
48 | 64 | }) |
|
74 | 90 | 'mb-2 flex w-full max-w-lg rounded-full border border-base-400 bg-base-100 p-[2px] pl-3 transition focus-within:shadow', |
75 | 91 | )} |
76 | 92 | id="site-input-container" |
77 | | - on:submit|preventDefault={() => addWebsite(inputWebsite, true)} |
| 93 | + novalidate |
| 94 | + on:submit|preventDefault={() => addWebsite(inputWebsite, { clear: true })} |
78 | 95 | > |
79 | 96 | <!-- svelte-ignore a11y-autofocus --> |
80 | 97 | <input |
81 | 98 | class="grow border-none bg-transparent outline-none placeholder:opacity-50 focus:ring-0" |
82 | | - type="text" |
| 99 | + type="url" |
83 | 100 | id="site-input" |
84 | 101 | name="site" |
85 | 102 | autofocus |
|
88 | 105 | /> |
89 | 106 | <Button>Add</Button> |
90 | 107 | </form> |
91 | | - {#if errorMessage} |
| 108 | + {#if validation == 'error'} |
92 | 109 | <div class="my-2" transition:slide> |
93 | 110 | <Callout kind="warning" title="Unable to add page"> |
94 | | - <button slot="top-right" on:click={() => (errorMessage = false)}> |
| 111 | + <button slot="top-right" on:click={() => (validation = 'idle')}> |
95 | 112 | <XMark /> |
96 | 113 | </button> |
97 | 114 |
|
98 | 115 | Unfortunately, we don't know about that site yet. |
99 | 116 | </Callout> |
100 | 117 | </div> |
| 118 | + {:else if validation == 'validating url'} |
| 119 | + <div class="my-2" transition:slide> |
| 120 | + <Callout kind="neutral" title="Validating URL..." /> |
| 121 | + </div> |
| 122 | + {:else if validation == 'loading similar'} |
| 123 | + <div class="my-2" transition:slide> |
| 124 | + <Callout kind="neutral" title="Fetching similar sites..." /> |
| 125 | + </div> |
101 | 126 | {/if} |
102 | 127 | <div class="flex flex-wrap justify-center gap-x-5 gap-y-3" id="sites-list"> |
103 | 128 | {#each chosenSites as site (site)} |
|
115 | 140 |
|
116 | 141 | {#if chosenSites.length > 0 && similarSites.length > 0} |
117 | 142 | <div class="flex flex-col space-y-4"> |
118 | | - <div class="grid grid-cols-[auto_auto_1fr_auto] items-center gap-5"> |
119 | | - <h2 class="text-2xl font-bold">Similar sites</h2> |
120 | | - <div class="flex space-x-1"> |
| 143 | + <div |
| 144 | + class="grid grid-cols-[auto_auto_1fr] items-center sm:grid-cols-[auto_auto_1fr] sm:gap-x-5 md:grid-rows-none" |
| 145 | + > |
| 146 | + <h2 class="text-lg font-bold sm:text-2xl">Similar sites</h2> |
| 147 | + <div class="flex space-x-1 justify-self-center"> |
121 | 148 | <Select |
122 | 149 | id="limit" |
123 | 150 | class="cursor-pointer rounded border-none dark:bg-transparent" |
124 | 151 | bind:value={limit} |
125 | 152 | options={LIMIT_OPTIONS.map((value) => ({ value, label: value.toString() }))} |
126 | 153 | /> |
127 | 154 | </div> |
128 | | - <div /> |
129 | | - <Button on:click={exportAsOptic}>Export as optic</Button> |
| 155 | + <div class="justify-self-end md:col-auto md:row-auto md:place-self-end"> |
| 156 | + <Button on:click={exportAsOptic}>Export as optic</Button> |
| 157 | + </div> |
130 | 158 | </div> |
131 | 159 | <div class="grid items-center gap-y-2"> |
132 | 160 | {#each similarSites as site (site.site)} |
|
141 | 169 | on:click={() => |
142 | 170 | chosenSites.includes(site.site) |
143 | 171 | ? removeWebsite(site.site) |
144 | | - : addWebsite(site.site)} |
| 172 | + : addWebsite(site.site, { validate: false })} |
145 | 173 | > |
146 | 174 | <PlusCircleOutline |
147 | 175 | class={twJoin( |
|
0 commit comments