From b60c707b80c31455e5d4672be16f40fcd1b413b7 Mon Sep 17 00:00:00 2001
From: Justin Deal <justindeal@protonmail.com>
Date: Tue, 6 May 2025 18:41:42 -0700
Subject: [PATCH] Update Icons to change with theme and resolve errors in
 console

---
 config/services.json                         | 105 ++++++++++---------
 public/service-worker.js                     |  16 ++-
 src/components/common/ScrollReveal.astro     |  32 ++++++
 src/components/common/ServiceCard.astro      |  36 +++++--
 src/components/homelab/CategorySection.astro |   1 +
 src/layouts/Layout.astro                     |  34 +++++-
 src/lib/config.ts                            |   1 +
 src/lib/types.ts                             |   9 +-
 8 files changed, 170 insertions(+), 64 deletions(-)

diff --git a/config/services.json b/config/services.json
index 64d843f..1606fd4 100644
--- a/config/services.json
+++ b/config/services.json
@@ -8,66 +8,89 @@
     }
   ],
 
-  "Development": [
+  "Storage": [
+    {
+      "name": "Nextcloud",
+      "link": "https://cloud.justin.deal",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/nextcloud-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/nextcloud-dark.svg",
+      "alt": "Nextcloud"
+    },
     {
       "name": "Gitea",
       "link": "https://code.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea.svg",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea-dark.svg",
       "alt": "Gitea",
       "tags": ["git", "code", "repository"]
     },
     {
       "name": "OpenGist",
       "link": "https://snippets.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/opengist.svg",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/opengist-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/opengist-dark.svg",
       "alt": "OpenGist",
       "tags": ["gist", "snippets"]
     },
+    {
+      "name": "Calibre-Web",
+      "link": "https://books.justin.deal",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/calibre-web-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/calibre-web-dark.svg",
+      "alt": "Calibre-Web",
+      "tags": ["books", "read"]
+    }
+  ],
+
+  "Utilities": [
+    {
+      "name": "Searxng",
+      "link": "https://search.justin.deal",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/searxng-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/searxng-dark.svg",
+      "alt": "Searxng",
+      "tags": ["search", "privacy", "metasearch"]
+    },
     {
       "name": "IT-Tools",
       "link": "https://tools.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/it-tools.svg",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/it-tools-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/it-tools-dark.svg",
       "alt": "IT-Tools",
       "tags": ["dev"]
     },
     {
       "name": "Ollama",
       "link": "https://ai.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/ollama.svg",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/ollama-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/ollama-dark.svg",
       "alt": "Ollama",
       "tags": ["LLM", "AI", "models", "chatbot"]
     }
-  ],  
-  
-  "Entertainment": [
-    {
-      "name": "Calibre-Web",
-      "link": "https://books.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/calibre.svg",
-      "alt": "Calibre-Web",
-      "tags": ["books", "read"]
-    }
   ],
 
   "Analytics & Monitoring": [
     {
       "name": "Uptime Kuma",
       "link": "https://status.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/uptime-kuma.svg",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/uptime-kuma-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/uptime-kuma-dark.svg",
       "alt": "Uptime Kuma",
       "tags": ["status"]
     },
     {
       "name": "Umami",
       "link": "https://analytics.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/umami.svg",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/umami-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/umami-dark.svg",
       "alt": "Umami",
       "tags": ["analytics"]
     },
     {
       "name": "TeslaMate",
       "link": "https://tesla.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/teslamate.svg",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/teslamate-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/teslamate-dark.svg",
       "alt": "TeslaMate",
       "tags": ["car", "tesla"]
     }
@@ -76,63 +99,51 @@
   "Infrastructure": [
     {
       "name": "Pi-hole",
-      "link": "https://pihole.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/pi-hole.svg",
+      "link": "http://pi.hole/admin/",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/pi-hole-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/pi-hole-dark.svg",
       "alt": "Pi-hole",
       "tags": ["dns"]
     },
     {
       "name": "Ntfy",
       "link": "https://ntfy.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/ntfy.svg",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/ntfy-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/ntfy-dark.svg",
       "alt": "Ntfy",
       "tags": ["notifications"]
     },
     {
       "name": "Vaultwarden",
       "link": "https://passwords.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/vaultwarden.svg",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/vaultwarden-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/vaultwarden-dark.svg",
       "alt": "Vaultwarden",
       "tags": ["passwords"]
     },
     {
       "name": "Authentik",
       "link": "https://auth.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/authentik.svg",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/authentik-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/authentik-dark.svg",
       "alt": "Authentik",
       "tags": ["SSO", "Auth", "Authentication"]
     },
     {
       "name": "Traefik",
       "link": "https://proxy.justin.deal:8080",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/traefik.svg",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/traefik-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/traefik-dark.svg",
       "alt": "Traefik",
       "tags": ["proxy", "reverse-proxy", "load-balancer"]
-    }
-  ],
-
-
-  "Utilities": [
-    {
-      "name": "Searxng",
-      "link": "https://search.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/searxng.svg",
-      "alt": "Searxng",
-      "tags": ["search", "privacy", "metasearch"]
     },
     {
-      "name": "Silverbullet",
-      "link": "https://notes.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/png/silverbullet.png",
-      "alt": "Silverbullet",
-      "tags": ["notes", "markdown", "knowledge base"]
-    },
-    {
-      "name": "Vikunja",
-      "link": "https://todo.justin.deal",
-      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/vikunja.svg",
-      "alt": "Vikunja",
-      "tags": ["todo", "tasks", "productivity"]
+      "name": "Syncthing",
+      "link": "https://sync.justin.deal",
+      "icon": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/syncthing-light.svg",
+      "iconDark": "https://cdn.jsdelivr.net/gh/selfhst/icons/svg/syncthing-dark.svg",
+      "alt": "Syncthing",
+      "tags": ["sync", "files"]
     }
   ]
 }
diff --git a/public/service-worker.js b/public/service-worker.js
index 8447086..ff8606c 100644
--- a/public/service-worker.js
+++ b/public/service-worker.js
@@ -389,11 +389,17 @@ self.addEventListener('message', event => {
           cacheNames.map(cacheName => caches.delete(cacheName))
         );
       }).then(() => {
-        // Notify client that caches were cleared
-        event.ports[0].postMessage({ 
-          status: 'success',
-          message: 'All caches cleared successfully'
-        });
+        // Notify client that caches were cleared, but only if port is available
+        if (event.ports && event.ports.length > 0) {
+          try {
+            event.ports[0].postMessage({ 
+              status: 'success',
+              message: 'All caches cleared successfully'
+            });
+          } catch (err) {
+            console.log('Could not post message to client: port may be closed');
+          }
+        }
       })
     );
   }
diff --git a/src/components/common/ScrollReveal.astro b/src/components/common/ScrollReveal.astro
index 900967a..b8699d4 100644
--- a/src/components/common/ScrollReveal.astro
+++ b/src/components/common/ScrollReveal.astro
@@ -73,6 +73,9 @@ const id = `scroll-reveal-${crypto.randomUUID().slice(0, 8)}`;
     
     if (!scrollRevealElements.length) return;
     
+    // Store observers to properly disconnect them later
+    const observers = new Map();
+    
     // Check if IntersectionObserver is supported
     if ('IntersectionObserver' in window) {
       scrollRevealElements.forEach(element => {
@@ -96,6 +99,8 @@ const id = `scroll-reveal-${crypto.randomUUID().slice(0, 8)}`;
                 // Unobserve if once is true
                 if (once) {
                   observer.unobserve(entry.target);
+                  // Remove from observers map to allow garbage collection
+                  observers.delete(entry.target);
                 }
               } else if (!once) {
                 // Remove class if element leaves viewport and once is false
@@ -109,9 +114,36 @@ const id = `scroll-reveal-${crypto.randomUUID().slice(0, 8)}`;
           }
         );
         
+        // Store observer reference for cleanup
+        observers.set(element, observer);
+        
         // Start observing
         observer.observe(element);
       });
+      
+      // Cleanup function to disconnect observers when page changes or component unmounts
+      const cleanup = () => {
+        observers.forEach((observer, element) => {
+          observer.unobserve(element);
+          observer.disconnect();
+        });
+        observers.clear();
+      };
+      
+      // Clean up observers when page is about to unload
+      window.addEventListener('beforeunload', cleanup);
+      
+      // For SPA navigation (if applicable)
+      document.addEventListener('astro:before-swap', cleanup);
+      
+      // Additional cleanup for any framework-specific unmounting
+      document.addEventListener('astro:after-swap', () => {
+        // Re-initialize on page navigation for SPAs
+        const newScrollRevealElements = document.querySelectorAll('.scroll-reveal');
+        if (newScrollRevealElements.length) {
+          // This will be handled by the DOMContentLoaded event in the new page
+        }
+      });
     } else {
       // Fallback for browsers that don't support IntersectionObserver
       scrollRevealElements.forEach(element => {
diff --git a/src/components/common/ServiceCard.astro b/src/components/common/ServiceCard.astro
index d7e4026..fd41673 100644
--- a/src/components/common/ServiceCard.astro
+++ b/src/components/common/ServiceCard.astro
@@ -7,7 +7,8 @@
  * <ServiceCard 
  *   name="Gitea" 
  *   href="https://code.justin.deal" 
- *   img="https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea.svg" 
+ *   img="https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea-light.svg" 
+ *   imgDark="https://cdn.jsdelivr.net/gh/selfhst/icons/svg/gitea-dark.svg" 
  *   alt="Gitea"
  * />
  * ```
@@ -24,17 +25,26 @@ interface Props {
   href: string;
   
   /**
-   * The URL of the service icon
+   * The URL of the service icon (light version)
    */
   img: string;
   
+  /**
+   * The URL of the service icon for dark mode (dark version)
+   * If not provided, falls back to the light version
+   */
+  imgDark?: string;
+  
   /**
    * Alternative text for the service icon
    */
   alt: string;
 }
 
-const { name, href, img, alt } = Astro.props;
+const { name, href, img, imgDark, alt } = Astro.props;
+
+// Use the provided dark icon or fall back to the light icon
+const darkIcon = imgDark || img;
 
 // Generate a unique ID for the service card
 const cardId = `service-card-${crypto.randomUUID().slice(0, 8)}`;
@@ -50,10 +60,22 @@ const cardId = `service-card-${crypto.randomUUID().slice(0, 8)}`;
 >
   <div class="service-icon-container flex-shrink-0 relative">
     <div class="service-icon-background"></div>
+    <!-- Light icon (shown in dark mode) -->
     <img
       src={img}
       alt={alt}
-      class="service-icon"
+      class="service-icon dark-theme-only"
+      loading="lazy"
+      decoding="async"
+      width="64"
+      height="64"
+      fetchpriority="low"
+    />
+    <!-- Dark icon (shown in light mode) -->
+    <img
+      src={darkIcon}
+      alt={alt}
+      class="service-icon light-theme-only"
       loading="lazy"
       decoding="async"
       width="64"
@@ -104,8 +126,8 @@ const cardId = `service-card-${crypto.randomUUID().slice(0, 8)}`;
       opacity var(--card-transition-duration) var(--card-transition-timing),
       padding var(--card-transition-duration) var(--card-transition-timing);
     
-    /* Performance optimizations */
-    will-change: transform, box-shadow, border-color, background-color, opacity;
+  /* Performance optimizations - reduced will-change usage to prevent high memory consumption */
+  /* Only use will-change on hover to reduce memory usage */
   }
   
   /* Gradient background effect */
@@ -193,6 +215,8 @@ const cardId = `service-card-${crypto.randomUUID().slice(0, 8)}`;
     border-color: var(--color-zag-accent);
     background-color: var(--color-zag-bg-hover);
     z-index: 10;
+    /* Apply will-change only on hover to reduce memory consumption */
+    will-change: transform, box-shadow;
   }
   
   .service-card:hover::before {
diff --git a/src/components/homelab/CategorySection.astro b/src/components/homelab/CategorySection.astro
index 082558c..d3d2352 100644
--- a/src/components/homelab/CategorySection.astro
+++ b/src/components/homelab/CategorySection.astro
@@ -102,6 +102,7 @@ const categoryLower = category.toLowerCase();
                   name={app.name}
                   href={app.link}
                   img={app.icon}
+                  imgDark={app.iconDark}
                   alt={app.name}
                 />
               </div>
diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro
index ca02ffc..99a9713 100644
--- a/src/layouts/Layout.astro
+++ b/src/layouts/Layout.astro
@@ -50,10 +50,32 @@ import "../styles/global.css";
           navigator.serviceWorker.register('/service-worker.js')
             .then(registration => {
               console.log('SW registered: ', registration.scope);
+              
+              // Handle communication errors
+              navigator.serviceWorker.addEventListener('message', event => {
+                // Process messages from service worker
+                if (event.data && event.data.type) {
+                  console.log('Message from SW:', event.data.type);
+                }
+              });
+              
+              // Handle controller change
+              navigator.serviceWorker.addEventListener('controllerchange', () => {
+                console.log('Service worker controller changed');
+              });
             })
             .catch(error => {
               console.log('SW registration failed: ', error);
             });
+            
+          // Handle communication errors globally
+          window.addEventListener('error', event => {
+            if (event.message && event.message.includes('postMessage')) {
+              console.log('Caught postMessage error:', event.message);
+              // Prevent the error from bubbling up
+              event.preventDefault();
+            }
+          });
         });
       }
     </script>
@@ -92,13 +114,15 @@ import "../styles/global.css";
       rel="stylesheet"
     />
     
-    <!-- Preload critical font files -->
+    <!-- Preload and include Press Start 2P font -->
     <link 
       rel="preload" 
-      href="https://cdn.jsdelivr.net/fontsource/fonts/press-start-2p@latest/latin-400-normal.woff2" 
-      as="font" 
-      type="font/woff2" 
-      crossorigin 
+      href="https://cdn.jsdelivr.net/npm/@fontsource/press-start-2p/index.css" 
+      as="style"
+    />
+    <link 
+      rel="stylesheet" 
+      href="https://cdn.jsdelivr.net/npm/@fontsource/press-start-2p/index.css"
     />
     
     <!-- Preload Alpine.js -->
diff --git a/src/lib/config.ts b/src/lib/config.ts
index f1de7fe..f9427f5 100644
--- a/src/lib/config.ts
+++ b/src/lib/config.ts
@@ -74,6 +74,7 @@ export interface Service {
   name: string;
   link: string;
   icon: string;
+  iconDark?: string;
   alt: string;
   tags?: string[];
 }
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 11b34cb..e2e575e 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -103,10 +103,17 @@ export type Service = {
   link: string;
   
   /**
-   * The URL to the service icon
+   * The URL to the service icon (light version)
+   * Used in dark theme
    */
   icon: string;
   
+  /**
+   * The URL to the dark version of the service icon
+   * Used in light theme
+   */
+  iconDark?: string;
+  
   /**
    * Alternative text for the service icon
    */