search for tags/files/text now possible
authorPhiTux <redacted>
Sat, 15 Mar 2025 13:01:50 +0000 (14:01 +0100)
committerPhiTux <redacted>
Sat, 15 Mar 2025 13:01:50 +0000 (14:01 +0100)
backend/server/routers/logs.py
backend/server/utils/fileHandling.py
frontend/src/lib/Sidenav.svelte
frontend/src/lib/searchStore.js
frontend/src/lib/tagStore.js [new file with mode: 0644]
frontend/src/routes/write/+page.svelte

index 7f017b57000d079e6e92e77025d38039b5b4d478..45686c2627d34adc4d398ede50dad744e254ba26 100644 (file)
@@ -1,9 +1,7 @@
-import base64
 import datetime
-import io
 import logging
 import re
-from fastapi import APIRouter, Cookie, Depends, Form, UploadFile, File, HTTPException
+from fastapi import APIRouter, Depends, Form, UploadFile, File, HTTPException
 from fastapi.responses import StreamingResponse
 from pydantic import BaseModel
 from . import users
@@ -11,7 +9,6 @@ from ..utils import fileHandling
 from ..utils import security
 import html
 from typing import Annotated
-import time
 
 
 logger = logging.getLogger("dailytxtLogger")
@@ -30,7 +27,7 @@ async def saveLog(log: Log, cookie = Depends(users.isLoggedIn)):
     month = datetime.datetime.fromisoformat(log.date).month
     day = datetime.datetime.fromisoformat(log.date).day
 
-    content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+    content:dict = fileHandling.getMonth(cookie["user_id"], year, month)
     
     # move old log to history
     if "days" in content.keys():
@@ -86,7 +83,7 @@ async def getLog(date: str, cookie = Depends(users.isLoggedIn)):
     month = datetime.datetime.fromisoformat(date).month
     day = datetime.datetime.fromisoformat(date).day
 
-    content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+    content:dict = fileHandling.getMonth(cookie["user_id"], year, month)
     
     dummy = {"text": "", "date_written": "", "files": [], "tags": []}
 
@@ -104,7 +101,6 @@ async def getLog(date: str, cookie = Depends(users.isLoggedIn)):
             if "files" in dayLog.keys():
                 for file in dayLog["files"]:
                     file["filename"] = security.decrypt_text(file["enc_filename"], enc_key)
-                    file["type"] = security.decrypt_text(file["enc_type"], enc_key)
             return {"text": text, "date_written": date_written, "files": dayLog.get("files", []), "tags": dayLog.get("tags", [])}
 
     return dummy
@@ -138,6 +134,13 @@ def get_end_index(text, index):
     return endIndex
 
 
+def get_begin(text):
+    # get first 5 words
+    words = text.split()
+    if len(words) < 5:
+        return text
+    return " ".join(words[:5])
+
 def get_context(text: str, searchString: str, exact: bool):
     # replace whitespace with non-breaking space
     text = re.sub(r'\s+', " ", text)
@@ -154,8 +157,8 @@ def get_context(text: str, searchString: str, exact: bool):
     return text[start:pos] + "<b>" + text[pos:pos+len(searchString)] + "</b>" + text[pos+len(searchString):end]
 
 
-@router.get("/search")
-async def search(searchString: str, cookie = Depends(users.isLoggedIn)):
+@router.get("/searchString")
+async def searchString(searchString: str, cookie = Depends(users.isLoggedIn)):
     results = []
     
     enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
@@ -163,10 +166,20 @@ async def search(searchString: str, cookie = Depends(users.isLoggedIn)):
     # search in all years and months (dirs)
     for year in fileHandling.get_years(cookie["user_id"]):
         for month in fileHandling.get_months(cookie["user_id"], year):
-            content:dict = fileHandling.getDay(cookie["user_id"], year, int(month))
+            content:dict = fileHandling.getMonth(cookie["user_id"], year, int(month))
             if "days" not in content.keys():
                 continue
             for dayLog in content["days"]:
+                if "text" not in dayLog.keys():
+                    if "files" in dayLog.keys():
+                        for file in dayLog["files"]:
+                            filename = security.decrypt_text(file["enc_filename"], enc_key)
+                            if searchString.lower() in filename.lower():
+                                context = "📎 " + filename
+                                results.append({"year": year, "month": month, "day": dayLog["day"], "text": context})
+                                break
+                    continue
+                
                 text = security.decrypt_text(dayLog["text"], enc_key)
                 
                 # "..." -> exact
@@ -198,18 +211,51 @@ async def search(searchString: str, cookie = Depends(users.isLoggedIn)):
                     if searchString.lower() in text.lower():
                         context = get_context(text, searchString, False)
                         results.append({"year": year, "month": month, "day": dayLog["day"], "text": context})
+                    
+                    elif "files" in dayLog.keys():
+                        for file in dayLog["files"]:
+                            filename = security.decrypt_text(file["enc_filename"], enc_key)
+                            if searchString.lower() in filename.lower():
+                                context = "📎 " + filename
+                                results.append({"year": year, "month": month, "day": dayLog["day"], "text": context})
+                                break
                         
         
     # sort by year and month and day
     results.sort(key=lambda x: (int(x["year"]), int(x["month"]), int(x["day"])), reverse=False)
     return results
 
+@router.get("/searchTag")
+async def searchTag(tag_id: int, cookie = Depends(users.isLoggedIn)):
+    results = []
+    enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
+
+    # search in all years and months (dirs)
+    for year in fileHandling.get_years(cookie["user_id"]):
+        for month in fileHandling.get_months(cookie["user_id"], year):
+            content:dict = fileHandling.getMonth(cookie["user_id"], year, int(month))
+            if "days" not in content.keys():
+                continue
+            for dayLog in content["days"]:
+                if "tags" not in dayLog.keys():
+                    continue
+                if tag_id in dayLog["tags"]:
+                    context = ''
+                    if "text" in dayLog.keys():
+                        text = security.decrypt_text(dayLog["text"], enc_key)
+                        context = get_begin(text)
+                    results.append({"year": year, "month": month, "day": dayLog["day"], "text": context})
+    
+    # sort by year and month and day
+    results.sort(key=lambda x: (int(x["year"]), int(x["month"]), int(x["day"])), reverse=False)
+    return results
+
 @router.get("/getMarkedDays")
 async def getMarkedDays(month: str, year: str, cookie = Depends(users.isLoggedIn)):
     days_with_logs = []
     days_with_files = []
 
-    content:dict = fileHandling.getDay(cookie["user_id"], year, int(month))
+    content:dict = fileHandling.getMonth(cookie["user_id"], year, int(month))
     if "days" not in content.keys():
         return {"days_with_logs": [], "days_with_files": []}
 
@@ -224,7 +270,7 @@ async def getMarkedDays(month: str, year: str, cookie = Depends(users.isLoggedIn
 
 @router.get("/loadMonthForReading")
 async def loadMonthForReading(month: int, year: int, cookie = Depends(users.isLoggedIn)):
-    content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+    content:dict = fileHandling.getMonth(cookie["user_id"], year, month)
     if "days" not in content.keys():
         return []
     
@@ -253,7 +299,7 @@ async def uploadFile(day: Annotated[int, Form()], month: Annotated[int, Form()],
         return {"success": False}
     
     # save file in log
-    content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+    content:dict = fileHandling.getMonth(cookie["user_id"], year, month)
 
     enc_filename = security.encrypt_text(file.filename, enc_key)
     new_file = {"enc_filename": enc_filename, "uuid_filename": uuid, "size": file.size}
@@ -280,29 +326,10 @@ async def uploadFile(day: Annotated[int, Form()], month: Annotated[int, Form()],
 
     return {"success": True}
 
-"""
-@router.get("/getFiles")
-async def getFiles(day: int, month: int, year: int, cookie = Depends(users.isLoggedIn)):
-    content:dict = fileHandling.getDay(cookie["user_id"], year, month)
-    if "days" not in content.keys():
-        return []
-    
-    enc_key = security.get_enc_key(cookie["user_id"], cookie["derived_key"])
-    for dayLog in content["days"]:
-        if "day" in dayLog.keys() and dayLog["day"] == day:
-            if "files" in dayLog.keys():
-                for file in dayLog["files"]:
-                    file["filename"] = security.decrypt_text(file["enc_filename"], enc_key)
-                    file["type"] = security.decrypt_text(file["enc_type"], enc_key)
-    
-                return dayLog["files"]
-
-    return []
-"""
 
 @router.get("/deleteFile")
 async def deleteFile(uuid: str, day: int, month: int, year: int, cookie = Depends(users.isLoggedIn)):
-    content:dict = fileHandling.getDay(cookie["user_id"], year, month)
+    content:dict = fileHandling.getMonth(cookie["user_id"], year, month)
     if "days" not in content.keys():
         raise HTTPException(status_code=500, detail="Day not found - json error")
     
@@ -385,7 +412,7 @@ class AddTagToLog(BaseModel):
 
 @router.post("/addTagToLog")
 async def addTagToLog(data: AddTagToLog, cookie = Depends(users.isLoggedIn)):
-    content:dict = fileHandling.getDay(cookie["user_id"], data.year, data.month)
+    content:dict = fileHandling.getMonth(cookie["user_id"], data.year, data.month)
     if "days" not in content.keys():
         content["days"] = []
     
@@ -411,7 +438,7 @@ async def addTagToLog(data: AddTagToLog, cookie = Depends(users.isLoggedIn)):
 
 @router.post("/removeTagFromLog")
 async def removeTagFromLog(data: AddTagToLog, cookie = Depends(users.isLoggedIn)):
-    content:dict = fileHandling.getDay(cookie["user_id"], data.year, data.month)
+    content:dict = fileHandling.getMonth(cookie["user_id"], data.year, data.month)
     if "days" not in content.keys():
         raise HTTPException(status_code=500, detail="Day not found - json error")
     
index 67410f5d5ff57a75e464e75ddd7b57df005c4470..a6397e6410de329075768224205c378c28b869ba 100644 (file)
@@ -23,7 +23,7 @@ def getUsers():
                 return {}
             return json.loads(s)
 
-def getDay(user_id, year, month):
+def getMonth(user_id, year, month):
     try:
         f = open(os.path.join(settings.data_path, f"{user_id}/{year}/{month:02d}.json"), "r")
     except FileNotFoundError:
index 5bbb8d101787b4df02cce972490ca7d9562041bd..9d0df73c7282ed60ec6ff52fada5032db3837230 100644 (file)
@@ -1,9 +1,23 @@
 <script>
        import Datepicker from './Datepicker.svelte';
-       import { searchString, searchResults } from '$lib/searchStore.js';
+       import { searchString, searchTag, searchResults, isSearching } from '$lib/searchStore.js';
        import { selectedDate } from '$lib/calendarStore.js';
+       import { tags } from '$lib/tagStore.js';
+       import { faQuestionCircle } from '@fortawesome/free-solid-svg-icons';
+       import { Fa } from 'svelte-fa';
+       import { onMount } from 'svelte';
+       import * as bootstrap from 'bootstrap';
+       import Tag from './Tag.svelte';
 
-       export let search = () => {};
+       let { searchForString, searchForTag } = $props();
+
+       onMount(() => {
+               const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
+               const popoverList = [...popoverTriggerList].map(
+                       (popoverTriggerEl) =>
+                               new bootstrap.Popover(popoverTriggerEl, { html: true, trigger: 'focus' })
+               );
+       });
 
        let searchInput;
        let ctrlPressed = false;
                }
                if (event.key === 'f' && ctrlPressed) {
                        event.preventDefault();
-                       searchInput.focus();
+                       $searchTag = {};
+                       setTimeout(() => {
+                               searchInput.focus();
+                       }, 100);
                }
        }
 
                        ctrlPressed = false;
                }
        }
+
+       let showTagDropdown = $state(false);
+       let filteredTags = $state([]);
+       let selectedTagIndex = $state(0);
+
+       function handleKeyDown(event) {
+               if (!showTagDropdown && event.key === 'Enter') searchForString();
+               if (filteredTags.length === 0) return;
+
+               switch (event.key) {
+                       case 'ArrowDown':
+                               event.preventDefault(); // Prevent cursor movement
+                               selectedTagIndex = Math.min(selectedTagIndex + 1, filteredTags.length - 1);
+                               ensureSelectedVisible();
+                               break;
+
+                       case 'ArrowUp':
+                               event.preventDefault(); // Prevent cursor movement
+                               selectedTagIndex = Math.max(selectedTagIndex - 1, 0);
+                               ensureSelectedVisible();
+                               break;
+
+                       case 'Enter':
+                               if (selectedTagIndex >= 0 && selectedTagIndex < filteredTags.length) {
+                                       event.preventDefault();
+                                       selectSearchTag(filteredTags[selectedTagIndex].id);
+                               }
+                               document.activeElement.blur();
+                               break;
+
+                       case 'Escape':
+                               showTagDropdown = false;
+                               break;
+               }
+       }
+
+       function ensureSelectedVisible() {
+               setTimeout(() => {
+                       for (let i = 0; i < 2; i++) {
+                               const dropdown = document.querySelectorAll('.searchTagDropdown')[i];
+                               const selectedElement = dropdown?.querySelector('.searchTag-item.selected');
+
+                               if (dropdown && selectedElement) {
+                                       const dropdownRect = dropdown.getBoundingClientRect();
+                                       const selectedRect = selectedElement.getBoundingClientRect();
+
+                                       if (selectedRect.top < dropdownRect.top) {
+                                               dropdown.scrollTop -= dropdownRect.top - selectedRect.top;
+                                       } else if (selectedRect.bottom > dropdownRect.bottom) {
+                                               dropdown.scrollTop += selectedRect.bottom - dropdownRect.bottom;
+                                       }
+                               }
+                       }
+               }, 40);
+       }
+
+       $effect(() => {
+               let search = $searchString;
+               if (search.startsWith('#')) {
+                       search = search.slice(1);
+                       showTagDropdown = true;
+                       filteredTags = $tags.filter((tag) => tag.name.toLowerCase().includes(search.toLowerCase()));
+               } else {
+                       showTagDropdown = false;
+               }
+       });
+
+       //let searchTag = $state({});
+       function selectSearchTag(tagId) {
+               const tag = $tags.find((tag) => tag.id === tagId);
+               if (!tag) {
+                       return;
+               }
+               $searchTag = tag;
+               //$searchResults = [];
+               showTagDropdown = false;
+
+               searchForTag();
+       }
+
+       function removeSearchTag() {
+               $searchTag = {};
+               $searchResults = [];
+       }
 </script>
 
 <svelte:window onkeydown={on_key_down} onkeyup={on_key_up} />
        <Datepicker />
        <br />
 
-       <form onsubmit={search} class="input-group mt-5">
-               <input
-                       bind:value={$searchString}
-                       bind:this={searchInput}
-                       id="search-input"
-                       type="text"
-                       class="form-control"
-                       placeholder="Suche"
-                       aria-label="Suche"
-                       aria-describedby="search-button"
-               />
-               <button class="btn btn-outline-secondary" type="submit" id="search-button">Suche</button>
-       </form>
-       <div class="list-group flex-grow-1 mb-2">
-               {#each $searchResults as result}
+       <div class="search">
+               <form onsubmit={searchForString} class="input-group mt-5">
                        <button
-                               type="button"
-                               onclick={() => {
-                                       $selectedDate = new Date(Date.UTC(result.year, result.month - 1, result.day));
-                               }}
-                               class="list-group-item list-group-item-action {$selectedDate.toDateString() ===
-                               new Date(Date.UTC(result.year, result.month - 1, result.day)).toDateString()
-                                       ? 'active'
-                                       : ''}"
+                               class="btn btn-outline-secondary"
+                               data-bs-toggle="popover"
+                               data-bs-title="Suche"
+                               data-bs-content="Hier kannst "
+                               onclick={(event) => event.preventDefault()}><Fa icon={faQuestionCircle} /></button
                        >
-                               <div class="search-result-content">
-                                       <div class="date">
-                                               {new Date(result.year, result.month - 1, result.day).toLocaleDateString('locale', {
-                                                       day: '2-digit',
-                                                       month: '2-digit',
-                                                       year: 'numeric'
-                                               })}
+                       {#if $searchTag.id}
+                               <!-- If a tag is selected ... -->
+                               <div class="ms-1 align-content-center">
+                                       <Tag tag={$searchTag} removeTag={removeSearchTag} isRemovable />
+                               </div>
+                               {#if $isSearching}
+                                       <div class="ms-2 align-content-center">
+                                               <div class="spinner-border spinner-border-sm" role="status">
+                                                       <span class="visually-hidden">Loading...</span>
+                                               </div>
                                        </div>
-                                       <!-- <div class="search-separator"></div> -->
-                                       <div class="text">
-                                               {@html result.text}
+                               {/if}
+                       {:else}
+                               <input
+                                       bind:value={$searchString}
+                                       bind:this={searchInput}
+                                       id="search-input"
+                                       type="text"
+                                       class="form-control"
+                                       placeholder="Suche"
+                                       aria-label="Suche"
+                                       aria-describedby="search-button"
+                                       onkeydown={handleKeyDown}
+                                       autocomplete="off"
+                                       onfocus={() => {
+                                               selectedTagIndex = 0;
+                                               if ($searchString.startsWith('#')) {
+                                                       showTagDropdown = true;
+                                               }
+                                       }}
+                                       onfocusout={() => {
+                                               setTimeout(() => (showTagDropdown = false), 150);
+                                       }}
+                               />
+                               <button class="btn btn-outline-secondary" type="submit" id="search-button">
+                                       {#if $isSearching}
+                                               <span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
+                                       {:else}
+                                               Suche
+                                       {/if}
+                               </button>
+                       {/if}
+               </form>
+               {#if showTagDropdown}
+                       <div class="searchTagDropdown">
+                               {#if filteredTags.length === 0}
+                                       <em style="padding: 0.2rem;">Kein Tag gefunden...</em>
+                               {:else}
+                                       {#each filteredTags as tag, index (tag.id)}
+                                               <!-- svelte-ignore a11y_click_events_have_key_events -->
+                                               <!-- svelte-ignore a11y_no_static_element_interactions -->
+                                               <!-- svelte-ignore a11y_mouse_events_have_key_events -->
+                                               <div
+                                                       role="button"
+                                                       tabindex="0"
+                                                       onclick={() => selectSearchTag(tag.id)}
+                                                       onmouseover={() => (selectedTagIndex = index)}
+                                                       class="searchTag-item {index === selectedTagIndex ? 'selected' : ''}"
+                                               >
+                                                       <Tag {tag} />
+                                               </div>
+                                       {/each}
+                               {/if}
+                       </div>
+               {/if}
+               <div class="list-group flex-grow-1 mb-2">
+                       {#each $searchResults as result}
+                               <button
+                                       type="button"
+                                       onclick={() => {
+                                               $selectedDate = new Date(Date.UTC(result.year, result.month - 1, result.day));
+                                       }}
+                                       class="list-group-item list-group-item-action {$selectedDate.toDateString() ===
+                                       new Date(Date.UTC(result.year, result.month - 1, result.day)).toDateString()
+                                               ? 'active'
+                                               : ''}"
+                               >
+                                       <div class="search-result-content">
+                                               <div class="date">
+                                                       {new Date(result.year, result.month - 1, result.day).toLocaleDateString('locale', {
+                                                               day: '2-digit',
+                                                               month: '2-digit',
+                                                               year: 'numeric'
+                                                       })}
+                                               </div>
+                                               <!-- <div class="search-separator"></div> -->
+                                               <div class="text">
+                                                       {@html result.text}
+                                               </div>
                                        </div>
-                               </div>
-                       </button>
-               {/each}
+                               </button>
+                       {/each}
+               </div>
        </div>
 </div>
 
 <style>
+       .searchTagDropdown {
+               position: absolute;
+               background-color: white;
+               box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+               z-index: 1000;
+               left: 60px;
+               max-height: 150px;
+               overflow-y: scroll;
+               overflow-x: hidden;
+               display: flex;
+               flex-direction: column;
+       }
+
+       .searchTag-item.selected {
+               background-color: #b2b4b6;
+       }
+
+       .searchTag-item {
+               cursor: pointer;
+               padding: 5px;
+       }
+
        .search-result-content {
                display: flex;
                align-items: center;
index b21437acfb90ef89f98d7e01ce0cc55e296f6913..eeb9527ecb66216b7a31a7e374bb3b00941e401b 100644 (file)
@@ -1,4 +1,6 @@
 import { writable } from "svelte/store";
 
 export let searchString = writable("");
-export let searchResults = writable([]);
\ No newline at end of file
+export let searchResults = writable([]);
+export let searchTag = writable({});
+export let isSearching = writable(false);
\ No newline at end of file
diff --git a/frontend/src/lib/tagStore.js b/frontend/src/lib/tagStore.js
new file mode 100644 (file)
index 0000000..1e6cc57
--- /dev/null
@@ -0,0 +1,3 @@
+import {writable} from 'svelte/store';
+
+export let tags = writable([]);
\ No newline at end of file
index b552e5712725e72b195f2a4d5a43267e586a3c83..daa35c12c06211ad242bd34ff0fb6a7020053035 100644 (file)
@@ -6,7 +6,7 @@
        import axios from 'axios';
        import { goto } from '$app/navigation';
        import { onMount } from 'svelte';
-       import { searchString, searchResults } from '$lib/searchStore.js';
+       import { searchString, searchTag, searchResults, isSearching } from '$lib/searchStore.js';
        import * as TinyMDE from 'tiny-markdown-editor';
        import '../../../node_modules/tiny-markdown-editor/dist/tiny-mde.css';
        import { API_URL } from '$lib/APIurl.js';
@@ -22,6 +22,7 @@
        import { v4 as uuidv4 } from 'uuid';
        import { slide, fade } from 'svelte/transition';
        import { autoLoadImages } from '$lib/settingsStore';
+       import { tags } from '$lib/tagStore';
        import Tag from '$lib/Tag.svelte';
        import TagModal from '$lib/TagModal.svelte';
 
                );
        });
 
-       let tags = $state([]);
        function loadTags() {
                axios
                        .get(API_URL + '/logs/getTags')
                        .then((response) => {
-                               tags = response.data;
+                               $tags = response.data;
                        })
                        .catch((error) => {
                                console.error(error);
                        event.preventDefault();
                        altPressed = false;
                }
+               if (event.key === 'Control') {
+                       event.preventDefault();
+                       ctrlPressed = false;
+               }
        }
 
        function changeDay(increment) {
                }
        });
 
-       let isSearching = $state(false);
-       function search() {
-               console.log($searchString);
-
-               if (isSearching) {
+       function searchForString() {
+               if ($isSearching) {
                        return;
                }
-               isSearching = true;
+               $isSearching = true;
 
                axios
-                       .get(API_URL + '/logs/search', {
+                       .get(API_URL + '/logs/searchString', {
                                params: {
                                        searchString: $searchString
                                }
                        })
                        .then((response) => {
                                $searchResults = [...response.data];
-                               isSearching = false;
+                               $isSearching = false;
                        })
                        .catch((error) => {
                                $searchResults = [];
                                console.error(error);
-                               isSearching = false;
+                               $isSearching = false;
+
+                               // toast
+                               const toast = new bootstrap.Toast(document.getElementById('toastErrorSearching'));
+                               toast.show();
+                       });
+       }
+
+       function searchForTag() {
+               $searchString = '';
+               if ($isSearching) {
+                       return;
+               }
+               $isSearching = true;
 
+               axios
+                       .get(API_URL + '/logs/searchTag', { params: { tag_id: $searchTag.id } })
+                       .then((response) => {
+                               $searchResults = [...response.data];
+                               $isSearching = false;
+                       })
+                       .catch((error) => {
+                               $isSearching = false;
+                               $searchResults = [];
+
+                               console.error(error);
                                // toast
                                const toast = new bootstrap.Toast(document.getElementById('toastErrorSearching'));
                                toast.show();
 
        // show the correct tags in the dropdown
        $effect(() => {
-               if (tags.length === 0) {
+               if ($tags.length === 0) {
                        filteredTags = [];
                        return;
                }
 
                // exclude already selected tags
-               let tagsWithoutSelected = tags.filter(
+               let tagsWithoutSelected = $tags.filter(
                        (tag) => !selectedTags.find((selectedTag) => selectedTag === tag.id)
                );
 
                                event.preventDefault(); // Prevent cursor movement
                                selectedTagIndex = Math.min(selectedTagIndex + 1, filteredTags.length - 1);
                                ensureSelectedVisible();
-                               console.log(selectedTagIndex);
                                break;
 
                        case 'ArrowUp':
                                        dropdown.scrollTop += selectedRect.bottom - dropdownRect.bottom;
                                }
                        }
-               }, 0);
+               }, 40);
        }
 
        let showTagLoading = $state(false);
                        aria-label="Close"
                ></button>
        </div>
-       <Sidenav {search} />
+       <Sidenav {searchForString} {searchForTag} />
 </div>
 
 <div class="d-flex flex-row justify-content-between h-100">
        <!-- shown on large Screen -->
        <div class="d-md-block d-none sidenav p-3">
-               <Sidenav {search} />
+               <Sidenav {searchForString} {searchForTag} />
        </div>
 
        <!-- Center -->
                                        <Fa icon={faQuestionCircle} fw /></a
                                >
                        </div>
-                       <div class="d-flex flex-row">
+                       <div class="tagRow d-flex flex-row">
                                <input
                                        bind:value={searchTab}
                                        onfocus={() => {
                                        placeholder="Tag..."
                                />
                                <button
-                                       class="btn btn-outline-secondary ms-2"
+                                       class="newTagBtn btn btn-outline-secondary ms-2"
                                        onclick={() => {
                                                openTagModal(null);
                                        }}
                                </div>
                        {/if}
                        <div class="selectedTags d-flex flex-row flex-wrap">
-                               {#if tags.length !== 0}
+                               {#if $tags.length !== 0}
                                        {#each selectedTags as tag_id (tag_id)}
                                                <div transition:slide={{ axis: 'x' }}>
-                                                       <Tag tag={tags.find((tag) => tag.id === tag_id)} {removeTag} isRemovable="true" />
+                                                       <Tag tag={$tags.find((tag) => tag.id === tag_id)} {removeTag} isRemovable="true" />
                                                </div>
                                        {/each}
                                {/if}
 </div>
 
 <style>
+       .tagRow {
+               width: 100%;
+       }
+
+       .newTagBtn {
+               white-space: nowrap;
+       }
+
        .tag-item.selected {
                background-color: #b2b4b6;
        }
git clone https://git.99rst.org/PROJECT