From dd81200520cdb533440c1a29a55d410ef95789c6 Mon Sep 17 00:00:00 2001
From: smian1 <smian1@gmail.com>
Date: Wed, 4 Dec 2024 00:50:58 -0800
Subject: [PATCH 1/2] Refactor metadata handling and improve API calls in OMI
 Apps

- Introduced envConfig for dynamic URL management across the application.
- Updated metadata generation functions to utilize envConfig for URLs, enhancing maintainability and consistency.
- Improved API call caching strategy in app-list component for better performance.
- Cleaned up metadata schemas to streamline structured data generation for SEO.
- Enhanced Open Graph and Twitter card metadata for improved social sharing and visibility.
---
 frontend/src/app/apps/[id]/page.tsx           | 20 ++++--
 frontend/src/app/apps/components/app-list.tsx |  4 +-
 frontend/src/app/apps/page.tsx                | 32 ++++-----
 frontend/src/app/apps/utils/metadata.ts       | 68 +++++++------------
 .../app/components/product-banner/index.tsx   |  6 +-
 frontend/src/hooks/useLocalStorage.ts         |  2 +-
 frontend/src/lib/api/apps.ts                  | 36 +++++-----
 7 files changed, 80 insertions(+), 88 deletions(-)

diff --git a/frontend/src/app/apps/[id]/page.tsx b/frontend/src/app/apps/[id]/page.tsx
index c518918e3d..e00b71837e 100644
--- a/frontend/src/app/apps/[id]/page.tsx
+++ b/frontend/src/app/apps/[id]/page.tsx
@@ -9,6 +9,7 @@ import { Calendar, User, FolderOpen, Puzzle } from 'lucide-react';
 import { Metadata, ResolvingMetadata } from 'next';
 import { ProductBanner } from '@/src/app/components/product-banner';
 import { getAppById, getAppsByCategory } from '@/src/lib/api/apps';
+import envConfig from '@/src/constants/envConfig';
 
 type Props = {
   params: { id: string };
@@ -28,21 +29,26 @@ export async function generateMetadata(
   }
 
   const categoryName = formatCategoryName(plugin.category);
-  const canonicalUrl = `https://omi.me/apps/${plugin.id}`;
-  const appStoreUrl = 'https://apps.apple.com/us/app/friend-ai-wearable/id6502156163';
-  const playStoreUrl = 'https://play.google.com/store/apps/details?id=com.friend.ios';
+  const canonicalUrl = `${envConfig.WEB_URL}/apps/${plugin.id}`;
 
   return {
     title: `${plugin.name} - ${categoryName} App | Omi`,
     description: `${plugin.description} Available on Omi, the AI-powered wearable platform.`,
-    metadataBase: new URL('https://omi.me'),
+    metadataBase: new URL(envConfig.WEB_URL),
     alternates: {
       canonical: canonicalUrl,
     },
     openGraph: {
       title: `${plugin.name} - ${categoryName} App`,
       description: plugin.description,
-      images: [plugin.image],
+      images: [
+        {
+          url: plugin.image,
+          width: 1200,
+          height: 630,
+          alt: `${plugin.name} App for Omi`,
+        },
+      ],
       url: canonicalUrl,
       type: 'website',
       siteName: 'Omi',
@@ -52,8 +58,8 @@ export async function generateMetadata(
       title: `${plugin.name} - ${categoryName} App`,
       description: plugin.description,
       images: [plugin.image],
-      creator: '@omi',
-      site: '@omi',
+      creator: '@omiHQ',
+      site: '@omiHQ',
     },
     other: {
       'application-name': 'Omi',
diff --git a/frontend/src/app/apps/components/app-list.tsx b/frontend/src/app/apps/components/app-list.tsx
index 213cc3bfdc..775b79ff29 100644
--- a/frontend/src/app/apps/components/app-list.tsx
+++ b/frontend/src/app/apps/components/app-list.tsx
@@ -9,12 +9,12 @@ import { ScrollableCategoryNav } from './scrollable-category-nav';
 async function getPluginsData() {
   const [pluginsResponse, statsResponse] = await Promise.all([
     fetch(`${envConfig.API_URL}/v1/approved-apps?include_reviews=true`, {
-      cache: 'no-store',
+      next: { revalidate: 3600 },
     }),
     fetch(
       'https://raw.githubusercontent.com/BasedHardware/omi/refs/heads/main/community-plugin-stats.json',
       {
-        cache: 'no-store',
+        next: { revalidate: 3600 },
       },
     ),
   ]);
diff --git a/frontend/src/app/apps/page.tsx b/frontend/src/app/apps/page.tsx
index 8bf6a75ad0..7c985f13d0 100644
--- a/frontend/src/app/apps/page.tsx
+++ b/frontend/src/app/apps/page.tsx
@@ -9,6 +9,7 @@ import {
 } from './utils/metadata';
 import { ProductBanner } from '../components/product-banner';
 import { getApprovedApps } from '@/src/lib/api/apps';
+import envConfig from '@/src/constants/envConfig';
 
 async function getAppsCount() {
   const plugins = await getApprovedApps();
@@ -19,27 +20,28 @@ export async function generateMetadata(): Promise<Metadata> {
   const appsCount = await getAppsCount();
   const title = 'OMI Apps Marketplace - AI-Powered Apps for Your OMI Necklace';
   const description = `Discover and install ${appsCount}+ AI-powered apps for your OMI Necklace. Browse apps across productivity, entertainment, health, and more. Transform your OMI experience with voice-controlled applications.`;
-  const baseMetadata = getBaseMetadata(title, description);
 
   return {
-    ...baseMetadata,
+    title,
+    description,
+    metadataBase: new URL(envConfig.WEB_URL),
     keywords:
       'OMI apps, AI apps, voice control apps, wearable apps, productivity apps, health apps, entertainment apps',
     alternates: {
-      canonical: 'https://omi.me/apps',
+      canonical: `${envConfig.WEB_URL}/apps`,
     },
     openGraph: {
       title,
       description,
-      url: 'https://omi.me/apps',
+      url: `${envConfig.WEB_URL}/apps`,
       siteName: 'OMI',
       images: [
         {
-          url: '/omi-app.png',
+          url: `${envConfig.WEB_URL}/omi-app.png`,
           width: 1200,
           height: 630,
           alt: 'OMI Apps Marketplace',
-        }
+        },
       ],
       locale: 'en_US',
       type: 'website',
@@ -48,7 +50,7 @@ export async function generateMetadata(): Promise<Metadata> {
       card: 'summary_large_image',
       title,
       description,
-      images: ['/omi-app.png'],
+      images: [`${envConfig.WEB_URL}/omi-app.png`],
       creator: '@omiHQ',
     },
     robots: {
@@ -59,15 +61,13 @@ export async function generateMetadata(): Promise<Metadata> {
         follow: true,
       },
     },
-    verification: {
-      other: {
-        'structured-data': JSON.stringify([
-          generateCollectionPageSchema(title, description, 'https://omi.me/apps'),
-          generateProductSchema(),
-          generateOrganizationSchema(),
-          generateBreadcrumbSchema(),
-        ]),
-      },
+    other: {
+      'structured-data': JSON.stringify([
+        generateCollectionPageSchema(),
+        generateProductSchema(),
+        generateOrganizationSchema(),
+        generateBreadcrumbSchema(),
+      ]),
     },
   };
 }
diff --git a/frontend/src/app/apps/utils/metadata.ts b/frontend/src/app/apps/utils/metadata.ts
index e74a402d3f..321949f49f 100644
--- a/frontend/src/app/apps/utils/metadata.ts
+++ b/frontend/src/app/apps/utils/metadata.ts
@@ -1,4 +1,5 @@
 import { Metadata } from 'next';
+import envConfig from '@/src/constants/envConfig';
 
 export interface CategoryMetadata {
   title: string;
@@ -94,13 +95,13 @@ export function generateBreadcrumbSchema(category?: string) {
         '@type': 'ListItem',
         position: 1,
         name: 'Home',
-        item: 'https://omi.me',
+        item: envConfig.WEB_URL,
       },
       {
         '@type': 'ListItem',
         position: 2,
         name: 'Apps',
-        item: 'https://omi.me/apps',
+        item: `${envConfig.WEB_URL}/apps`,
       },
     ],
   };
@@ -110,7 +111,7 @@ export function generateBreadcrumbSchema(category?: string) {
       '@type': 'ListItem',
       position: 3,
       name: categoryMetadata[category]?.title || category,
-      item: `https://omi.me/apps/category/${category}`,
+      item: `${envConfig.WEB_URL}/apps/category/${category}`,
     });
   }
 
@@ -123,47 +124,36 @@ export function generateProductSchema() {
     '@type': 'Product',
     name: productInfo.name,
     description: productInfo.description,
-    brand: {
-      '@type': 'Brand',
-      name: 'OMI',
-    },
+    image: `${envConfig.WEB_URL}/omi-app.png`,
     offers: {
       '@type': 'Offer',
       price: productInfo.price,
       priceCurrency: productInfo.currency,
-      availability: 'https://schema.org/InStock',
       url: productInfo.url,
+      availability: 'https://schema.org/InStock',
+    },
+    brand: {
+      '@type': 'Brand',
+      name: 'OMI',
     },
-    additionalProperty: [
-      {
-        '@type': 'PropertyValue',
-        name: 'App Store',
-        value: appStoreInfo.ios,
-      },
-      {
-        '@type': 'PropertyValue',
-        name: 'Play Store',
-        value: appStoreInfo.android,
-      },
-    ],
   };
 }
 
 export function generateCollectionPageSchema(
   title: string,
   description: string,
-  url: string,
+  canonicalUrl: string,
 ) {
   return {
     '@context': 'https://schema.org',
     '@type': 'CollectionPage',
-    name: title,
-    description: description,
-    url: url,
+    name: 'OMI Apps Marketplace',
+    description: 'Discover and install AI-powered apps for your OMI Necklace.',
+    url: `${envConfig.WEB_URL}/apps`,
     isPartOf: {
       '@type': 'WebSite',
       name: 'OMI Apps Marketplace',
-      url: 'https://omi.me/apps',
+      url: envConfig.WEB_URL,
     },
   };
 }
@@ -173,8 +163,13 @@ export function generateOrganizationSchema() {
     '@context': 'https://schema.org',
     '@type': 'Organization',
     name: 'OMI',
-    url: 'https://omi.me',
-    sameAs: [appStoreInfo.ios, appStoreInfo.android],
+    url: envConfig.WEB_URL,
+    logo: `${envConfig.WEB_URL}/omi-app.png`,
+    sameAs: [
+      'https://twitter.com/omiHQ',
+      'https://www.instagram.com/omi.me/',
+      'https://www.linkedin.com/company/omi-me/',
+    ],
   };
 }
 
@@ -205,23 +200,10 @@ export function getBaseMetadata(title: string, description: string): Metadata {
   return {
     title,
     description,
-    metadataBase: new URL('https://omi.me'),
-    openGraph: {
-      title,
-      description,
-      type: 'website',
-      siteName: 'OMI Apps Marketplace',
-    },
-    twitter: {
-      card: 'summary_large_image',
-      title,
-      description,
-      creator: '@omi',
-      site: '@omi',
-    },
+    metadataBase: new URL(envConfig.WEB_URL),
     other: {
-      'apple-itunes-app': `app-id=6502156163`,
-      'google-play-app': `app-id=com.friend.ios`,
+      'apple-itunes-app': `app-id=${appStoreInfo.ios.split('/id')[1]}`,
+      'google-play-app': `app-id=${appStoreInfo.android.split('id=')[1]}`,
     },
   };
 }
diff --git a/frontend/src/app/components/product-banner/index.tsx b/frontend/src/app/components/product-banner/index.tsx
index 9c5b741d9d..1d260ca0c3 100644
--- a/frontend/src/app/components/product-banner/index.tsx
+++ b/frontend/src/app/components/product-banner/index.tsx
@@ -230,7 +230,11 @@ export function ProductBanner({
                   </span>
                   <span className="inline-flex items-center gap-1 rounded-full bg-teal-500/10 px-2 py-0.5 text-xs text-teal-300">
                     <svg className="h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
-                      <path fillRule="evenodd" d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm5.771 7H5V5h10v7H8.771z" clipRule="evenodd" />
+                      <path
+                        fillRule="evenodd"
+                        d="M3 5a2 2 0 012-2h10a2 2 0 012 2v8a2 2 0 01-2 2h-2.22l.123.489.804.804A1 1 0 0113 18H7a1 1 0 01-.707-1.707l.804-.804L7.22 15H5a2 2 0 01-2-2V5zm5.771 7H5V5h10v7H8.771z"
+                        clipRule="evenodd"
+                      />
                     </svg>
                     <span className="whitespace-nowrap">Memory</span>
                   </span>
diff --git a/frontend/src/hooks/useLocalStorage.ts b/frontend/src/hooks/useLocalStorage.ts
index da7f4fa37a..5925e5dc69 100644
--- a/frontend/src/hooks/useLocalStorage.ts
+++ b/frontend/src/hooks/useLocalStorage.ts
@@ -35,4 +35,4 @@ export function useLocalStorage<T>(
   };
 
   return [storedValue, setValue];
-} 
\ No newline at end of file
+}
diff --git a/frontend/src/lib/api/apps.ts b/frontend/src/lib/api/apps.ts
index e0a7ca0b12..3c1cfd4133 100644
--- a/frontend/src/lib/api/apps.ts
+++ b/frontend/src/lib/api/apps.ts
@@ -1,6 +1,6 @@
-import { cache } from 'react'
-import envConfig from '@/src/constants/envConfig'
-import { Plugin, PluginStat } from '@/src/app/apps/components/types'
+import { cache } from 'react';
+import envConfig from '@/src/constants/envConfig';
+import { Plugin, PluginStat } from '@/src/app/apps/components/types';
 
 /**
  * Cached fetch utility for approved apps
@@ -12,39 +12,39 @@ export const getApprovedApps = cache(async () => {
       `${envConfig.API_URL}/v1/approved-apps?include_reviews=true`,
       {
         next: { revalidate: 21600 }, // 6 hours
-      }
-    )
+      },
+    );
 
     if (!response.ok) {
-      throw new Error(`Failed to fetch apps: ${response.statusText}`)
+      throw new Error(`Failed to fetch apps: ${response.statusText}`);
     }
 
-    const plugins = (await response.json()) as Plugin[]
-    return plugins
+    const plugins = (await response.json()) as Plugin[];
+    return plugins;
   } catch (error) {
-    console.error('Error fetching approved apps:', error)
-    throw error
+    console.error('Error fetching approved apps:', error);
+    throw error;
   }
-})
+});
 
 /**
  * Get a single app by ID using the cached data
  */
 export const getAppById = cache(async (id: string) => {
-  const plugins = await getApprovedApps()
-  return plugins.find((p) => p.id === id)
-})
+  const plugins = await getApprovedApps();
+  return plugins.find((p) => p.id === id);
+});
 
 /**
  * Get apps by category using the cached data
  */
 export const getAppsByCategory = cache(async (category: string) => {
-  const plugins = await getApprovedApps()
+  const plugins = await getApprovedApps();
   return category === 'integration'
     ? plugins.filter(
         (plugin) =>
           Array.isArray(plugin.capabilities) &&
-          plugin.capabilities.includes('external_integration')
+          plugin.capabilities.includes('external_integration'),
       )
-    : plugins.filter((plugin) => plugin.category === category)
-}) 
\ No newline at end of file
+    : plugins.filter((plugin) => plugin.category === category);
+});

From b42d6d58e59caf45685c00bef79864325c58df7d Mon Sep 17 00:00:00 2001
From: smian1 <smian1@gmail.com>
Date: Wed, 4 Dec 2024 20:04:15 -0800
Subject: [PATCH 2/2] Update dependencies and enhance app layout

- Added `lodash` and its type definitions to package dependencies for improved utility functions.
- Introduced a new `SearchBar` component in the app list for enhanced user search experience.
- Integrated `GoogleAnalytics` into the layout for better tracking of user interactions.
- Added a CSS class `.search-hidden` to manage visibility of search elements.

These changes aim to improve functionality and user experience across the application.
---
 frontend/package-lock.json                    | 14 +++
 frontend/package.json                         |  2 +
 frontend/src/app/apps/components/app-list.tsx |  4 +
 .../apps/components/plugin-card/compact.tsx   |  4 +
 .../apps/components/plugin-card/featured.tsx  |  4 +
 .../app/apps/components/search/search-bar.tsx | 98 +++++++++++++++++++
 frontend/src/app/globals.css                  |  4 +
 frontend/src/app/layout.tsx                   |  2 +
 .../components/shared/google-analytics.tsx    | 27 +++++
 9 files changed, 159 insertions(+)
 create mode 100644 frontend/src/app/apps/components/search/search-bar.tsx
 create mode 100644 frontend/src/components/shared/google-analytics.tsx

diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 67e0d0e254..ba7a3cc535 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -19,6 +19,7 @@
         "@radix-ui/react-tabs": "^1.1.0",
         "@radix-ui/react-tooltip": "^1.1.4",
         "@radix-ui/react-visually-hidden": "^1.1.0",
+        "@types/lodash": "^4.17.13",
         "algoliasearch": "^5.2.5",
         "class-variance-authority": "^0.7.0",
         "clsx": "^2.1.1",
@@ -26,6 +27,7 @@
         "gleap": "^13.9.2",
         "iconoir-react": "^7.8.0",
         "instantsearch.css": "^8.5.0",
+        "lodash": "^4.17.21",
         "lucide-react": "^0.438.0",
         "markdown-to-jsx": "^7.5.0",
         "moment": "^2.30.1",
@@ -2152,6 +2154,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/lodash": {
+      "version": "4.17.13",
+      "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz",
+      "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==",
+      "license": "MIT"
+    },
     "node_modules/@types/node": {
       "version": "20.16.1",
       "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz",
@@ -5369,6 +5377,12 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
     "node_modules/lodash.castarray": {
       "version": "4.4.0",
       "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 550dcf8709..b248c13e45 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -22,6 +22,7 @@
     "@radix-ui/react-tabs": "^1.1.0",
     "@radix-ui/react-tooltip": "^1.1.4",
     "@radix-ui/react-visually-hidden": "^1.1.0",
+    "@types/lodash": "^4.17.13",
     "algoliasearch": "^5.2.5",
     "class-variance-authority": "^0.7.0",
     "clsx": "^2.1.1",
@@ -29,6 +30,7 @@
     "gleap": "^13.9.2",
     "iconoir-react": "^7.8.0",
     "instantsearch.css": "^8.5.0",
+    "lodash": "^4.17.21",
     "lucide-react": "^0.438.0",
     "markdown-to-jsx": "^7.5.0",
     "moment": "^2.30.1",
diff --git a/frontend/src/app/apps/components/app-list.tsx b/frontend/src/app/apps/components/app-list.tsx
index 775b79ff29..10bd966b25 100644
--- a/frontend/src/app/apps/components/app-list.tsx
+++ b/frontend/src/app/apps/components/app-list.tsx
@@ -5,6 +5,7 @@ import { CategoryHeader } from './category-header';
 import type { Plugin, PluginStat } from './types';
 import { ChevronRight } from 'lucide-react';
 import { ScrollableCategoryNav } from './scrollable-category-nav';
+import { SearchBar } from './search/search-bar';
 
 async function getPluginsData() {
   const [pluginsResponse, statsResponse] = await Promise.all([
@@ -100,6 +101,9 @@ export default async function AppList() {
             <p className="mt-3 text-gray-400">
               Discover our most popular AI-powered applications
             </p>
+            <div className="mt-6">
+              <SearchBar />
+            </div>
           </div>
         </div>
 
diff --git a/frontend/src/app/apps/components/plugin-card/compact.tsx b/frontend/src/app/apps/components/plugin-card/compact.tsx
index 1279f5a5f9..96fb6be6e2 100644
--- a/frontend/src/app/apps/components/plugin-card/compact.tsx
+++ b/frontend/src/app/apps/components/plugin-card/compact.tsx
@@ -22,6 +22,10 @@ export function CompactPluginCard({ plugin, index }: CompactPluginCardProps) {
     <Link
       href={`/apps/${plugin.id}`}
       className="flex items-start gap-4 rounded-lg p-2 text-left transition-colors duration-300 hover:bg-[#1A1F2E]/50"
+      data-plugin-card
+      data-search-content={`${plugin.name} ${plugin.author} ${plugin.description}`}
+      data-categories={plugin.category}
+      data-capabilities={Array.from(plugin.capabilities).join(' ')}
     >
       {/* Index number */}
       <span className="w-6 text-sm font-medium text-gray-400">{index}</span>
diff --git a/frontend/src/app/apps/components/plugin-card/featured.tsx b/frontend/src/app/apps/components/plugin-card/featured.tsx
index 368dfb033b..2471fe4a43 100644
--- a/frontend/src/app/apps/components/plugin-card/featured.tsx
+++ b/frontend/src/app/apps/components/plugin-card/featured.tsx
@@ -22,6 +22,10 @@ export function FeaturedPluginCard({ plugin, hideStats }: FeaturedPluginCardProp
     <Link
       href={`/apps/${plugin.id}`}
       className="group relative block overflow-hidden rounded-xl bg-[#1A1F2E]"
+      data-plugin-card
+      data-search-content={`${plugin.name} ${plugin.author} ${plugin.description}`}
+      data-categories={plugin.category}
+      data-capabilities={Array.from(plugin.capabilities).join(' ')}
     >
       {/* Image */}
       <div className="aspect-[16/9] w-full overflow-hidden">
diff --git a/frontend/src/app/apps/components/search/search-bar.tsx b/frontend/src/app/apps/components/search/search-bar.tsx
new file mode 100644
index 0000000000..640b6ae513
--- /dev/null
+++ b/frontend/src/app/apps/components/search/search-bar.tsx
@@ -0,0 +1,98 @@
+'use client';
+
+import { Search, X } from 'lucide-react';
+import { useCallback, useState, useEffect } from 'react';
+import { cn } from '@/src/lib/utils';
+import debounce from 'lodash/debounce';
+
+interface SearchBarProps {
+  className?: string;
+}
+
+export function SearchBar({ className }: SearchBarProps) {
+  const [searchQuery, setSearchQuery] = useState('');
+  const [isFocused, setIsFocused] = useState(false);
+
+  const handleSearch = useCallback((query: string) => {
+    setSearchQuery(query);
+    const searchContent = query.toLowerCase().trim();
+    const cards = document.querySelectorAll('[data-plugin-card]');
+    cards.forEach((card) => {
+      const content = card.getAttribute('data-search-content')?.toLowerCase() || '';
+      const categories = card.getAttribute('data-categories')?.toLowerCase() || '';
+      const capabilities = card.getAttribute('data-capabilities')?.toLowerCase() || '';
+      if (
+        searchContent === '' ||
+        content.includes(searchContent) ||
+        categories.includes(searchContent) ||
+        capabilities.includes(searchContent)
+      ) {
+        card.classList.remove('search-hidden');
+      } else {
+        card.classList.add('search-hidden');
+      }
+    });
+
+    document.querySelectorAll('section').forEach((section) => {
+      const visibleCards = section.querySelectorAll(
+        '[data-plugin-card]:not(.search-hidden)',
+      );
+      if (visibleCards.length === 0) {
+        section.classList.add('search-hidden');
+      } else {
+        section.classList.remove('search-hidden');
+      }
+    });
+  }, []);
+
+  const debouncedSearch = useCallback(
+    debounce((query: string) => handleSearch(query), 150),
+    [handleSearch],
+  );
+
+  const clearSearch = useCallback(() => {
+    setSearchQuery('');
+    handleSearch('');
+  }, [handleSearch]);
+
+  useEffect(() => {
+    return () => {
+      debouncedSearch.cancel();
+    };
+  }, [debouncedSearch]);
+
+  return (
+    <div className={cn('relative mx-auto w-full max-w-2xl', className)}>
+      <div className="group relative">
+        <Search
+          className={cn(
+            'absolute left-4 top-1/2 h-4 w-4 -translate-y-1/2 transition-colors',
+            isFocused || searchQuery
+              ? 'text-[#6C8EEF]'
+              : 'text-gray-400 group-hover:text-[#6C8EEF]',
+          )}
+        />
+        <input
+          type="text"
+          value={searchQuery}
+          onChange={(e) => {
+            setSearchQuery(e.target.value);
+            debouncedSearch(e.target.value);
+          }}
+          onFocus={() => setIsFocused(true)}
+          onBlur={() => setIsFocused(false)}
+          placeholder="Search apps, categories, or capabilities..."
+          className="h-12 w-full rounded-full bg-[#1A1F2E] pl-11 pr-11 text-sm text-white placeholder-gray-400 outline-none ring-1 ring-white/5 transition-all hover:ring-white/10 focus:bg-[#242938] focus:ring-[#6C8EEF]/50"
+        />
+        {searchQuery && (
+          <button
+            onClick={clearSearch}
+            className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 transition-colors hover:text-white"
+          >
+            <X className="h-4 w-4" />
+          </button>
+        )}
+      </div>
+    </div>
+  );
+} 
\ No newline at end of file
diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css
index 11f10bf7ee..4b845aacd2 100644
--- a/frontend/src/app/globals.css
+++ b/frontend/src/app/globals.css
@@ -29,3 +29,7 @@
   animation: gradient-x 15s ease infinite;
   background-size: 400% 400%;
 }
+
+.search-hidden {
+  display: none !important;
+}
diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx
index 8e42d3685c..0bdb2bd817 100644
--- a/frontend/src/app/layout.tsx
+++ b/frontend/src/app/layout.tsx
@@ -5,6 +5,7 @@ import AppHeader from '../components/shared/app-header';
 import Footer from '../components/shared/footer';
 import envConfig from '../constants/envConfig';
 import { GleapInit } from '@/src/components/shared/gleap';
+import { GoogleAnalytics } from '@/src/components/shared/google-analytics';
 
 const inter = Mulish({
   subsets: ['latin'],
@@ -36,6 +37,7 @@ export default function RootLayout({
         <Footer />
       </body>
       <GleapInit />
+      <GoogleAnalytics />
     </html>
   );
 }
diff --git a/frontend/src/components/shared/google-analytics.tsx b/frontend/src/components/shared/google-analytics.tsx
new file mode 100644
index 0000000000..aadee3a4fc
--- /dev/null
+++ b/frontend/src/components/shared/google-analytics.tsx
@@ -0,0 +1,27 @@
+'use client';
+
+import Script from 'next/script';
+
+export function GoogleAnalytics() {
+  if (process.env.NODE_ENV !== 'production') {
+    return null;
+  }
+
+  return (
+    <>
+      <Script
+        src="https://www.googletagmanager.com/gtag/js?id=G-2WSLB4VPWF"
+        strategy="afterInteractive"
+      />
+      <Script id="google-analytics" strategy="afterInteractive">
+        {`
+          window.dataLayer = window.dataLayer || [];
+          function gtag(){dataLayer.push(arguments);}
+          gtag('js', new Date());
+
+          gtag('config', 'G-2WSLB4VPWF');
+        `}
+      </Script>
+    </>
+  );
+} 
\ No newline at end of file