|
11 | 11 | Edit2, |
12 | 12 | Trash2, |
13 | 13 | Save, |
14 | | - X |
| 14 | + X, |
| 15 | + Search |
15 | 16 | } from '@lucide/svelte'; |
16 | 17 | import { slide } from 'svelte/transition'; |
17 | 18 | import { flip } from 'svelte/animate'; |
|
20 | 21 | let selectedCategoryId = $state<string | null>(null); |
21 | 22 | let newlyCreatedSnippetId = $state<string | null>(null); |
22 | 23 | let isReorderMode = $state(false); |
| 24 | + let searchQuery = $state(''); |
23 | 25 | let massDeleteModal: HTMLDialogElement; |
24 | 26 |
|
25 | 27 | function confirmMassDelete() { |
|
39 | 41 |
|
40 | 42 | for (const catId of categoryIds) { |
41 | 43 | const cat = dbStore.data.categories.find((c) => c.id === catId); |
42 | | - const snips = dbStore.data.snippets.filter((s) => s.categoryId === catId); |
| 44 | + let snips = dbStore.data.snippets.filter((s) => s.categoryId === catId); |
| 45 | + |
| 46 | + if (searchQuery.trim()) { |
| 47 | + const query = searchQuery.toLowerCase(); |
| 48 | + snips = snips.filter((s) => |
| 49 | + Object.values(s.content).some((val) => val.toLowerCase().includes(query)) |
| 50 | + ); |
| 51 | + } |
| 52 | +
|
43 | 53 | if ( |
44 | 54 | snips.length > 0 || |
45 | | - (selectedCategoryId === catId && dbStore.data.snippets.length > 0) |
| 55 | + (!searchQuery.trim() && selectedCategoryId === catId && dbStore.data.snippets.length > 0) |
46 | 56 | ) { |
47 | 57 | // We only show empty groups if it's the currently selected category and there are NO snippets at all, which is handled in the template. |
48 | 58 | groups.push({ categoryId: catId, category: cat, snippets: snips }); |
49 | 59 | } |
50 | 60 | } |
51 | 61 |
|
52 | 62 | if (!selectedCategoryId) { |
53 | | - const uncategorizedSnippets = dbStore.data.snippets.filter( |
| 63 | + let uncategorizedSnippets = dbStore.data.snippets.filter( |
54 | 64 | (s) => !dbStore.data.categories.find((c) => c.id === s.categoryId) |
55 | 65 | ); |
| 66 | + |
| 67 | + if (searchQuery.trim()) { |
| 68 | + const query = searchQuery.toLowerCase(); |
| 69 | + uncategorizedSnippets = uncategorizedSnippets.filter((s) => |
| 70 | + Object.values(s.content).some((val) => val.toLowerCase().includes(query)) |
| 71 | + ); |
| 72 | + } |
| 73 | +
|
56 | 74 | if (uncategorizedSnippets.length > 0) { |
57 | 75 | groups.push({ |
58 | 76 | categoryId: '', |
|
180 | 198 | }} |
181 | 199 | > |
182 | 200 | <button |
183 | | - class={!selectedCategoryId ? 'active text-left' : 'text-left'} |
| 201 | + class={!selectedCategoryId ? 'menu-active text-left' : 'text-left'} |
184 | 202 | onclick={() => (selectedCategoryId = null)} |
185 | 203 | > |
186 | 204 | All Snippets |
|
196 | 214 | }} |
197 | 215 | > |
198 | 216 | <button |
199 | | - class={`text-left transition-colors ${selectedCategoryId === category.id ? 'active' : ''}`} |
| 217 | + class={`text-left transition-colors ${selectedCategoryId === category.id ? 'menu-active' : ''}`} |
200 | 218 | onclick={() => (selectedCategoryId = category.id)} |
201 | 219 | > |
202 | 220 | {#if category.icon} |
|
224 | 242 | All Snippets |
225 | 243 | {/if} |
226 | 244 | </h1> |
227 | | - <div class="flex w-full items-center gap-2 sm:w-auto"> |
228 | | - <select |
| 245 | + <div class="flex w-full flex-col gap-3 sm:flex-row sm:w-auto sm:items-center"> |
| 246 | + <label class="input input-sm input-bordered flex items-center gap-2 w-full sm:w-48"> |
| 247 | + <Search class="h-4 w-4 opacity-50" /> |
| 248 | + <input |
| 249 | + type="text" |
| 250 | + class="grow" |
| 251 | + placeholder="Search snippets..." |
| 252 | + bind:value={searchQuery} |
| 253 | + /> |
| 254 | + </label> |
| 255 | + <div class="flex items-center gap-2 w-full sm:w-auto"> |
| 256 | + <select |
229 | 257 | class="select-bordered select font-bold select-sm" |
230 | 258 | value={dbStore.globalLanguageId} |
231 | 259 | onchange={(e) => dbStore.setGlobalLanguageId(e.currentTarget.value)} |
|
244 | 272 | <button class="btn btn-primary btn-sm" onclick={addSnippet}> |
245 | 273 | <Plus class="h-4 w-4" /> Add |
246 | 274 | </button> |
| 275 | + </div> |
247 | 276 | </div> |
248 | 277 | </div> |
249 | 278 |
|
|
281 | 310 |
|
282 | 311 | {#if groupedSnippets.length === 0} |
283 | 312 | <div class="rounded-box border border-dashed border-base-200 bg-base-100 py-12 text-center" |
| 313 | + transition:slide={{ duration: 300 }} |
284 | 314 | use:droppable={{ |
285 | 315 | container: selectedCategoryId || '', |
286 | 316 | disabled: !isReorderMode, |
287 | 317 | callbacks: { onDrop: handleDrop } |
288 | 318 | }}> |
289 | | - <p class="mb-4 text-base-content/60">No snippets found in this category.</p> |
290 | | - <button class="btn btn-outline btn-sm" onclick={addSnippet}> |
291 | | - Create First Snippet |
292 | | - </button> |
| 319 | + {#if searchQuery.trim()} |
| 320 | + <p class="text-base-content/60">No snippets found matching "{searchQuery}".</p> |
| 321 | + {:else} |
| 322 | + <p class="mb-4 text-base-content/60">No snippets found in this category.</p> |
| 323 | + <button class="btn btn-outline btn-sm" onclick={addSnippet}> |
| 324 | + Create First Snippet |
| 325 | + </button> |
| 326 | + {/if} |
293 | 327 | </div> |
294 | 328 | {:else} |
295 | 329 | <div class="flex flex-col gap-6"> |
|
0 commit comments