[READ-ONLY] Mirror of https://github.com/flo-bit/atmo-tools. atmo.tools
0

Configure Feed

Select the types of activity you want to include in your feed.

fix

+208 -159
+208 -159
src/routes/search/Search.svelte
··· 85 85 </script> 86 86 87 87 <svelte:window 88 - bind:scrollY={scrollY} 88 + bind:scrollY 89 89 onkeydown={() => { 90 90 const tag = document.activeElement?.tagName; 91 91 if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; ··· 93 93 }} 94 94 /> 95 95 96 - <a 97 - href="/" 98 - class="text-base-500 dark:text-base-400 hover:text-accent-600 dark:hover:text-accent-400 fixed top-4 left-4 z-50 inline-flex items-center gap-1 text-sm transition-colors" 99 - > 100 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4"> 101 - <path fill-rule="evenodd" d="M17 10a.75.75 0 0 1-.75.75H5.612l4.158 3.96a.75.75 0 1 1-1.04 1.08l-5.5-5.25a.75.75 0 0 1 0-1.08l5.5-5.25a.75.75 0 1 1 1.04 1.08L5.612 9.25H16.25A.75.75 0 0 1 17 10Z" clip-rule="evenodd" /> 102 - </svg> 103 - Back 104 - </a> 96 + <a 97 + href="/" 98 + class="text-base-500 dark:text-base-400 hover:text-accent-600 dark:hover:text-accent-400 fixed top-4 left-4 z-50 inline-flex items-center gap-1 text-sm transition-colors" 99 + > 100 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-4"> 101 + <path 102 + fill-rule="evenodd" 103 + d="M17 10a.75.75 0 0 1-.75.75H5.612l4.158 3.96a.75.75 0 1 1-1.04 1.08l-5.5-5.25a.75.75 0 0 1 0-1.08l5.5-5.25a.75.75 0 1 1 1.04 1.08L5.612 9.25H16.25A.75.75 0 0 1 17 10Z" 104 + clip-rule="evenodd" 105 + /> 106 + </svg> 107 + Back 108 + </a> 105 109 106 - <Navbar class="mx-2 py-3 h-auto max-w-xl flex-col items-start sm:mx-auto top-12 md:top-10"> 107 - <div class="mb-4 mx-2 flex items-baseline gap-2"> 108 - <span class="text-base-600 dark:text-base-400 text-sm font-medium">Search my</span> 109 - <div class="flex gap-1"> 110 - {#each ALL_SOURCES as source} 111 - <button 112 - class="cursor-pointer rounded-full px-3 py-1 text-sm font-medium transition-colors {searchState.activeSource === 113 - source 114 - ? 'bg-accent-600 text-white' 115 - : 'text-base-600 dark:text-base-400 hover:bg-base-200 dark:hover:bg-base-800'}" 116 - onclick={() => handleSwitchSource(source)} 117 - > 118 - {SOURCE_LABELS[source]} 119 - </button> 120 - {/each} 121 - </div> 110 + <Navbar class="top-12 mx-2 h-auto max-w-xl flex-col items-start py-3 sm:mx-auto md:top-10"> 111 + <div class="mx-2 mb-4 flex items-baseline gap-2"> 112 + <div class="flex gap-1"> 113 + {#each ALL_SOURCES as source} 114 + <button 115 + class="cursor-pointer rounded-full px-3 py-1 text-sm font-medium transition-colors {searchState.activeSource === 116 + source 117 + ? 'bg-accent-600 text-white' 118 + : 'text-base-600 dark:text-base-400 hover:bg-base-200 dark:hover:bg-base-800'}" 119 + onclick={() => handleSwitchSource(source)} 120 + > 121 + {SOURCE_LABELS[source]} 122 + </button> 123 + {/each} 122 124 </div> 125 + </div> 123 126 124 - <div class="flex w-full items-center gap-2"> 125 - <Input 126 - bind:ref={input} 127 - bind:value={search} 128 - class="w-full" 129 - sizeVariant="lg" 130 - placeholder={PLACEHOLDERS[searchState.activeSource]} 131 - /> 132 - <button 133 - class="text-base-500 dark:text-base-400 hover:text-accent-600 dark:hover:text-accent-400 relative cursor-pointer p-2 transition-colors" 134 - onclick={() => (showFilters = !showFilters)} 135 - aria-label="Toggle filters" 127 + <div class="flex w-full items-center gap-2"> 128 + <Input 129 + bind:ref={input} 130 + bind:value={search} 131 + class="w-full" 132 + sizeVariant="lg" 133 + placeholder={PLACEHOLDERS[searchState.activeSource]} 134 + /> 135 + <button 136 + class="text-base-500 dark:text-base-400 hover:text-accent-600 dark:hover:text-accent-400 relative cursor-pointer p-2 transition-colors" 137 + onclick={() => (showFilters = !showFilters)} 138 + aria-label="Toggle filters" 139 + > 140 + <svg 141 + xmlns="http://www.w3.org/2000/svg" 142 + viewBox="0 0 20 20" 143 + fill="currentColor" 144 + class="size-5" 136 145 > 137 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5"> 138 - <path fill-rule="evenodd" d="M2.628 1.601C5.028 1.206 7.49 1 10 1s4.973.206 7.372.601a.75.75 0 0 1 .628.74v2.288a2.25 2.25 0 0 1-.659 1.59l-4.682 4.683a2.25 2.25 0 0 0-.659 1.59v3.037c0 .684-.31 1.33-.844 1.757l-1.937 1.55A.75.75 0 0 1 8 18.25v-5.757a2.25 2.25 0 0 0-.659-1.591L2.659 6.22A2.25 2.25 0 0 1 2 4.629V2.34a.75.75 0 0 1 .628-.74Z" clip-rule="evenodd" /> 139 - </svg> 140 - {#if hasActiveFilters} 141 - <span class="bg-accent-600 absolute top-1 right-1 size-2 rounded-full"></span> 142 - {/if} 143 - </button> 144 - </div> 145 - 146 - {#if showFilters} 147 - <div class="mt-3 flex w-full flex-col gap-3 px-2"> 148 - {#if searchState.activeSource !== 'posts'} 149 - <div class="flex flex-col gap-1.5"> 150 - <div class="flex items-center gap-2"> 151 - <label class="text-base-500 dark:text-base-400 text-xs w-16 shrink-0">Handle</label> 152 - <AtprotoHandlePopup 153 - bind:value={handleInput} 154 - onselected={(actor) => { 155 - if (!filters.handles.includes(actor.handle)) { 156 - filters.handles = [...filters.handles, actor.handle]; 157 - } 158 - handleInput = ''; 159 - }} 160 - /> 161 - </div> 162 - {#if filters.handles.length > 0} 163 - <div class="ml-18 flex flex-wrap gap-1"> 164 - {#each filters.handles as handle} 165 - <span class="bg-base-200 dark:bg-base-800 text-base-700 dark:text-base-300 inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs"> 166 - @{handle} 167 - <button 168 - class="text-base-500 hover:text-base-800 dark:hover:text-base-100 cursor-pointer" 169 - onclick={() => (filters.handles = filters.handles.filter((h) => h !== handle))} 170 - aria-label="Remove {handle}" 171 - > 172 - <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" class="size-3"> 173 - <path d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z" /> 174 - </svg> 175 - </button> 176 - </span> 177 - {/each} 178 - </div> 179 - {/if} 180 - </div> 181 - {/if} 146 + <path 147 + fill-rule="evenodd" 148 + d="M2.628 1.601C5.028 1.206 7.49 1 10 1s4.973.206 7.372.601a.75.75 0 0 1 .628.74v2.288a2.25 2.25 0 0 1-.659 1.59l-4.682 4.683a2.25 2.25 0 0 0-.659 1.59v3.037c0 .684-.31 1.33-.844 1.757l-1.937 1.55A.75.75 0 0 1 8 18.25v-5.757a2.25 2.25 0 0 0-.659-1.591L2.659 6.22A2.25 2.25 0 0 1 2 4.629V2.34a.75.75 0 0 1 .628-.74Z" 149 + clip-rule="evenodd" 150 + /> 151 + </svg> 152 + {#if hasActiveFilters} 153 + <span class="bg-accent-600 absolute top-1 right-1 size-2 rounded-full"></span> 154 + {/if} 155 + </button> 156 + </div> 182 157 183 - <div class="flex items-center gap-3"> 184 - <div class="flex items-center gap-1"> 185 - <label class="text-base-500 dark:text-base-400 text-xs">Min likes</label> 186 - <input 187 - type="number" 188 - bind:value={filters.minLikes} 189 - min="0" 190 - class="bg-base-100 dark:bg-base-900 border-base-300 dark:border-base-700 text-base-800 dark:text-base-200 w-16 rounded-md border px-2 py-1 text-xs" 158 + {#if showFilters} 159 + <div class="mt-3 flex w-full flex-col gap-3 px-2"> 160 + {#if searchState.activeSource !== 'posts'} 161 + <div class="flex flex-col gap-1.5"> 162 + <div class="flex items-center gap-2"> 163 + <label class="text-base-500 dark:text-base-400 w-16 shrink-0 text-xs">Handle</label> 164 + <AtprotoHandlePopup 165 + bind:value={handleInput} 166 + onselected={(actor) => { 167 + if (!filters.handles.includes(actor.handle)) { 168 + filters.handles = [...filters.handles, actor.handle]; 169 + } 170 + handleInput = ''; 171 + }} 191 172 /> 192 173 </div> 193 - <div class="flex items-center gap-1"> 194 - <label class="text-base-500 dark:text-base-400 text-xs">Reposts</label> 195 - <input 196 - type="number" 197 - bind:value={filters.minReposts} 198 - min="0" 199 - class="bg-base-100 dark:bg-base-900 border-base-300 dark:border-base-700 text-base-800 dark:text-base-200 w-16 rounded-md border px-2 py-1 text-xs" 200 - /> 201 - </div> 202 - <div class="flex items-center gap-1"> 203 - <label class="text-base-500 dark:text-base-400 text-xs">Replies</label> 204 - <input 205 - type="number" 206 - bind:value={filters.minReplies} 207 - min="0" 208 - class="bg-base-100 dark:bg-base-900 border-base-300 dark:border-base-700 text-base-800 dark:text-base-200 w-16 rounded-md border px-2 py-1 text-xs" 209 - /> 210 - </div> 174 + {#if filters.handles.length > 0} 175 + <div class="ml-18 flex flex-wrap gap-1"> 176 + {#each filters.handles as handle} 177 + <span 178 + class="bg-base-200 dark:bg-base-800 text-base-700 dark:text-base-300 inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs" 179 + > 180 + @{handle} 181 + <button 182 + class="text-base-500 hover:text-base-800 dark:hover:text-base-100 cursor-pointer" 183 + onclick={() => (filters.handles = filters.handles.filter((h) => h !== handle))} 184 + aria-label="Remove {handle}" 185 + > 186 + <svg 187 + xmlns="http://www.w3.org/2000/svg" 188 + viewBox="0 0 16 16" 189 + fill="currentColor" 190 + class="size-3" 191 + > 192 + <path 193 + d="M5.28 4.22a.75.75 0 0 0-1.06 1.06L6.94 8l-2.72 2.72a.75.75 0 1 0 1.06 1.06L8 9.06l2.72 2.72a.75.75 0 1 0 1.06-1.06L9.06 8l2.72-2.72a.75.75 0 0 0-1.06-1.06L8 6.94 5.28 4.22Z" 194 + /> 195 + </svg> 196 + </button> 197 + </span> 198 + {/each} 199 + </div> 200 + {/if} 211 201 </div> 202 + {/if} 212 203 213 - <div class="flex items-center gap-3"> 214 - <div class="flex items-center gap-1"> 215 - <label class="text-base-500 dark:text-base-400 text-xs">After</label> 216 - <input 217 - type="date" 218 - bind:value={filters.dateAfter} 219 - class="bg-base-100 dark:bg-base-900 border-base-300 dark:border-base-700 text-base-800 dark:text-base-200 rounded-md border px-2 py-1 text-xs" 220 - /> 221 - </div> 222 - <div class="flex items-center gap-1"> 223 - <label class="text-base-500 dark:text-base-400 text-xs">Before</label> 224 - <input 225 - type="date" 226 - bind:value={filters.dateBefore} 227 - class="bg-base-100 dark:bg-base-900 border-base-300 dark:border-base-700 text-base-800 dark:text-base-200 rounded-md border px-2 py-1 text-xs" 228 - /> 229 - </div> 204 + <div class="flex items-center gap-3"> 205 + <div class="flex items-center gap-1"> 206 + <label class="text-base-500 dark:text-base-400 text-xs">Min likes</label> 207 + <input 208 + type="number" 209 + bind:value={filters.minLikes} 210 + min="0" 211 + class="bg-base-100 dark:bg-base-900 border-base-300 dark:border-base-700 text-base-800 dark:text-base-200 w-16 rounded-md border px-2 py-1 text-xs" 212 + /> 213 + </div> 214 + <div class="flex items-center gap-1"> 215 + <label class="text-base-500 dark:text-base-400 text-xs">Reposts</label> 216 + <input 217 + type="number" 218 + bind:value={filters.minReposts} 219 + min="0" 220 + class="bg-base-100 dark:bg-base-900 border-base-300 dark:border-base-700 text-base-800 dark:text-base-200 w-16 rounded-md border px-2 py-1 text-xs" 221 + /> 222 + </div> 223 + <div class="flex items-center gap-1"> 224 + <label class="text-base-500 dark:text-base-400 text-xs">Replies</label> 225 + <input 226 + type="number" 227 + bind:value={filters.minReplies} 228 + min="0" 229 + class="bg-base-100 dark:bg-base-900 border-base-300 dark:border-base-700 text-base-800 dark:text-base-200 w-16 rounded-md border px-2 py-1 text-xs" 230 + /> 230 231 </div> 232 + </div> 231 233 232 - <div class="flex items-center gap-2"> 233 - <span class="text-base-500 dark:text-base-400 text-xs">Has:</span> 234 - <button class={pillClass(filters.hasImage)} onclick={() => (filters.hasImage = !filters.hasImage)}>Image</button> 235 - <button class={pillClass(filters.hasLink)} onclick={() => (filters.hasLink = !filters.hasLink)}>Link</button> 236 - <button class={pillClass(filters.hasVideo)} onclick={() => (filters.hasVideo = !filters.hasVideo)}>Video</button> 234 + <div class="flex items-center gap-3"> 235 + <div class="flex items-center gap-1"> 236 + <label class="text-base-500 dark:text-base-400 text-xs">After</label> 237 + <input 238 + type="date" 239 + bind:value={filters.dateAfter} 240 + class="bg-base-100 dark:bg-base-900 border-base-300 dark:border-base-700 text-base-800 dark:text-base-200 rounded-md border px-2 py-1 text-xs" 241 + /> 242 + </div> 243 + <div class="flex items-center gap-1"> 244 + <label class="text-base-500 dark:text-base-400 text-xs">Before</label> 245 + <input 246 + type="date" 247 + bind:value={filters.dateBefore} 248 + class="bg-base-100 dark:bg-base-900 border-base-300 dark:border-base-700 text-base-800 dark:text-base-200 rounded-md border px-2 py-1 text-xs" 249 + /> 237 250 </div> 251 + </div> 238 252 239 - <div class="flex items-center gap-2"> 240 - <button class={pillClass(!filters.showReplies)} onclick={() => (filters.showReplies = !filters.showReplies)}>Hide replies</button> 241 - </div> 253 + <div class="flex items-center gap-2"> 254 + <span class="text-base-500 dark:text-base-400 text-xs">Has:</span> 255 + <button 256 + class={pillClass(filters.hasImage)} 257 + onclick={() => (filters.hasImage = !filters.hasImage)}>Image</button 258 + > 259 + <button 260 + class={pillClass(filters.hasLink)} 261 + onclick={() => (filters.hasLink = !filters.hasLink)}>Link</button 262 + > 263 + <button 264 + class={pillClass(filters.hasVideo)} 265 + onclick={() => (filters.hasVideo = !filters.hasVideo)}>Video</button 266 + > 267 + </div> 242 268 243 - {#if hasActiveFilters} 244 - <button 245 - class="text-base-500 dark:text-base-400 hover:text-accent-600 dark:hover:text-accent-400 cursor-pointer self-start text-xs underline" 246 - onclick={() => (filters = { ...DEFAULT_FILTERS })} 247 - > 248 - Clear filters 249 - </button> 250 - {/if} 269 + <div class="flex items-center gap-2"> 270 + <button 271 + class={pillClass(!filters.showReplies)} 272 + onclick={() => (filters.showReplies = !filters.showReplies)}>Hide replies</button 273 + > 251 274 </div> 252 - {/if} 253 275 254 - {@const src = searchState.sources[searchState.activeSource]} 255 - <span class="text-base-600 dark:text-base-400 mt-2 mx-4 text-xs"> 256 - {#if src.phase === 'idle'} 257 - loading... 258 - {:else if src.phase === 'fetching'} 259 - fetching {SOURCE_LABELS[searchState.activeSource].toLowerCase()}... ({src.count + src.pendingUris.length} found) 260 - {:else if src.phase === 'hydrating'} 261 - indexing {src.indexed}/{src.totalToIndex} {SOURCE_LABELS[searchState.activeSource].toLowerCase()}... ({src.count} searchable) 262 - {:else} 263 - {SOURCE_LABELS[searchState.activeSource].toLowerCase()}: {src.count}{#if results.length > 0}, {results.length} results{/if} 276 + {#if hasActiveFilters} 277 + <button 278 + class="text-base-500 dark:text-base-400 hover:text-accent-600 dark:hover:text-accent-400 cursor-pointer self-start text-xs underline" 279 + onclick={() => (filters = { ...DEFAULT_FILTERS })} 280 + > 281 + Clear filters 282 + </button> 264 283 {/if} 265 - </span> 266 - </Navbar> 284 + </div> 285 + {/if} 286 + 287 + {@const src = searchState.sources[searchState.activeSource]} 288 + <span class="text-base-600 dark:text-base-400 mx-4 mt-2 text-xs"> 289 + {#if src.phase === 'idle'} 290 + loading... 291 + {:else if src.phase === 'fetching'} 292 + fetching {SOURCE_LABELS[searchState.activeSource].toLowerCase()}... ({src.count + 293 + src.pendingUris.length} found) 294 + {:else if src.phase === 'hydrating'} 295 + indexing {src.indexed}/{src.totalToIndex} 296 + {SOURCE_LABELS[searchState.activeSource].toLowerCase()}... ({src.count} searchable) 297 + {:else} 298 + {SOURCE_LABELS[searchState.activeSource].toLowerCase()}: {src.count}{#if results.length > 0}, {results.length} 299 + results{/if} 300 + {/if} 301 + </span> 302 + </Navbar> 267 303 268 304 {#if results.length > 0} 269 - <ul class="{showFilters ? 'pt-90 md:pt-80' : 'pt-32 md:pt-20'} flex flex-col divide-y divide-base-200 dark:divide-base-800 text-sm"> 305 + <ul 306 + class="{showFilters 307 + ? 'pt-90 md:pt-80' 308 + : 'pt-32 md:pt-20'} divide-base-200 dark:divide-base-800 flex flex-col divide-y text-sm" 309 + > 270 310 {#each results as result (result.doc.uri)} 271 311 <li class="py-4"> 272 312 <BlueskyPost 273 - 274 313 liked={result.doc.sources?.includes('likes')} 275 314 bookmarked={result.doc.sources?.includes('bookmarks')} 276 315 showLogo ··· 287 326 {/if} 288 327 </div> 289 328 {:else if search || hasActiveFilters} 290 - <div class="text-base-600 dark:text-base-400 {showFilters ? 'pt-90 md:pt-80' : 'pt-32 md:pt-20'} text-center text-sm font-semibold">No results</div> 329 + <div 330 + class="text-base-600 dark:text-base-400 {showFilters 331 + ? 'pt-90 md:pt-80' 332 + : 'pt-32 md:pt-20'} text-center text-sm font-semibold" 333 + > 334 + No results 335 + </div> 291 336 {/if} 292 337 293 338 {#if scrollY > 300} ··· 297 342 aria-label="Scroll to top" 298 343 > 299 344 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5"> 300 - <path fill-rule="evenodd" d="M9.47 6.47a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 1 1-1.06 1.06L10 8.06l-3.72 3.72a.75.75 0 0 1-1.06-1.06l4.25-4.25Z" clip-rule="evenodd" /> 345 + <path 346 + fill-rule="evenodd" 347 + d="M9.47 6.47a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 1 1-1.06 1.06L10 8.06l-3.72 3.72a.75.75 0 0 1-1.06-1.06l4.25-4.25Z" 348 + clip-rule="evenodd" 349 + /> 301 350 </svg> 302 351 </button> 303 352 {/if}