AppManager: Show spinner during search

Search form doesn't throw an error when not on main page

Change-Id: I108c681101c3b03901205695ee3775fb18eaa900
diff --git a/core/installer/welcome/appmanager-tmpl/base.html b/core/installer/welcome/appmanager-tmpl/base.html
index 7944783..2d7b94e 100644
--- a/core/installer/welcome/appmanager-tmpl/base.html
+++ b/core/installer/welcome/appmanager-tmpl/base.html
@@ -3,7 +3,7 @@
 	<head>
 		<meta charset="utf-8" />
         <link rel="stylesheet" href="/stat/pico.2.0.6.min.css">
-        <link rel="stylesheet" type="text/css" href="/stat/appmanager.css?v=0.0.14">
+        <link rel="stylesheet" type="text/css" href="/stat/appmanager.css?v=0.0.15">
 		<meta name="viewport" content="width=device-width, initial-scale=1" />
 	</head>
 	<body>
@@ -26,6 +26,6 @@
 			  {{ block "content" . }}{{ end }}
 		  </div>
       </main>
-    <script src="/stat/app-manager.js?v=0.0.10"></script>
+    <script src="/stat/app-manager.js?v=0.0.11"></script>
 	</body>
 </html>
diff --git a/core/installer/welcome/appmanager-tmpl/index.html b/core/installer/welcome/appmanager-tmpl/index.html
index fcb613f..b3ca2ab 100644
--- a/core/installer/welcome/appmanager-tmpl/index.html
+++ b/core/installer/welcome/appmanager-tmpl/index.html
@@ -1,6 +1,6 @@
 {{ define "header" }}
   <form id="search-form" class="search-bar" method="GET" action="/{{ .SearchTarget }}">
-      <input id="search-input" name="query" type="search" placeholder="Search" value="{{ .SearchValue }}"/>
+      <input id="search-input" class="search-icon" name="query" type="search" placeholder="Search" value="{{ .SearchValue }}"/>
   </form>
   <input type="hidden" id="page-type" value="{{ .SearchTarget }}" />
 {{ end }}
diff --git a/core/installer/welcome/stat/app-manager.js b/core/installer/welcome/stat/app-manager.js
index 55357f3..9cafd83 100644
--- a/core/installer/welcome/stat/app-manager.js
+++ b/core/installer/welcome/stat/app-manager.js
@@ -9,46 +9,64 @@
 document.addEventListener("DOMContentLoaded", function () {
     let searchRequestCount = 0;
     const page = document.documentElement;
-    const headerHeight = parseFloat(getComputedStyle(page).getPropertyValue('--pico-header-height').replace("px", ""));
+    const headerHeight = parseFloat(getComputedStyle(page).getPropertyValue("--pico-header-height").replace("px", ""));
     const nav = document.getElementById("menu");
     const windowHeight = window.innerHeight - headerHeight;
     nav.style.setProperty("--max-height", `${windowHeight}px`);
     const menu = document.getElementById("menu-nav");
     const menuHeight = parseFloat(getComputedStyle(document.getElementById('menu-nav')).height.replace("px", "")) + 15;
     menu.style.setProperty("height", `${menuHeight}px`);
-    const searchForm = document.getElementById('search-form');
-    const searchInput = document.getElementById('search-input');
+    const searchForm = document.getElementById("search-form");
+    const searchInput = document.getElementById("search-input");
+    function startSearchAnimation() {
+        searchInput.classList.remove("search-icon");
+        searchInput.classList.add("search-progress-icon");
+    }
+    function stopSearchAnimation() {
+        searchInput.classList.remove("search-progress-icon");
+        searchInput.classList.add("search-icon");
+    }
     function fetchAndUpdateAppList() {
         searchRequestCount++;
         const currentRequest = searchRequestCount;
         const formData = new FormData(searchForm);
-        const query = formData.get('query');
-        const pageType = document.getElementById('page-type').value;
+        const query = formData.get("query");
+        const pageType = document.getElementById("page-type").value;
         const url = `/${pageType}?query=${encodeURIComponent(query)}`;
+        startSearchAnimation();
         fetch(url, {
-            method: 'GET'
+            method: "GET"
         })
             .then(response => response.text())
             .then(html => {
                 if (currentRequest !== searchRequestCount) {
                     return;
                 }
-                const tempDiv = document.createElement('div');
+                const tempDiv = document.createElement("div");
                 tempDiv.innerHTML = html;
-                const newAppListHTML = tempDiv.querySelector('#app-list').innerHTML;
+                const newAppListHTML = tempDiv.querySelector("#app-list").innerHTML;
                 const appListContainer = document.getElementById("app-list");
                 appListContainer.innerHTML = newAppListHTML;
             })
-            .catch(error => console.error('Error fetching app list:', error));
+            .catch(error => {
+                console.error("Error fetching app list:", error);
+            })
+            .finally(() => {
+                stopSearchAnimation();
+            });
     }
     const delayedFetchAndUpdateAppList = delaySearch(fetchAndUpdateAppList, 300);
-    searchForm.addEventListener('submit', (event) => {
-        event.preventDefault();
-        fetchAndUpdateAppList();
-    });
-    searchInput.addEventListener('input', () => {
-        delayedFetchAndUpdateAppList();
-    });
+    if (searchForm) {
+        searchForm.addEventListener("submit", (event) => {
+            event.preventDefault();
+            fetchAndUpdateAppList();
+        });
+    }
+    if (searchInput) {
+        searchInput.addEventListener("input", () => {
+            delayedFetchAndUpdateAppList();
+        });
+    }
 });
 
 let prevWindowHeight = window.innerHeight;
diff --git a/core/installer/welcome/stat/appmanager.css b/core/installer/welcome/stat/appmanager.css
index cde49ab..f8ee945 100644
--- a/core/installer/welcome/stat/appmanager.css
+++ b/core/installer/welcome/stat/appmanager.css
@@ -327,3 +327,11 @@
 .primary {
   color: #7f9f7f;
 }
+
+.search-icon {
+  background-image: var(--pico-icon-search) !important;
+}
+
+.search-progress-icon {
+  background-image: var(--pico-icon-loading) !important;
+}