Skip to content

Commit

Permalink
Added Search, GA and few other changes. (#1479)
Browse files Browse the repository at this point in the history
  • Loading branch information
smian1 authored Dec 5, 2024
2 parents 2f24fb9 + b42d6d5 commit ee071b3
Show file tree
Hide file tree
Showing 15 changed files with 239 additions and 88 deletions.
14 changes: 14 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
"@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",
"framer-motion": "^11.5.4",
"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",
Expand Down
20 changes: 13 additions & 7 deletions frontend/src/app/apps/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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',
Expand All @@ -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',
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/app/apps/components/app-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,17 @@ 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([
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 },
},
),
]);
Expand Down Expand Up @@ -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>

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/app/apps/components/plugin-card/compact.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/app/apps/components/plugin-card/featured.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down
98 changes: 98 additions & 0 deletions frontend/src/app/apps/components/search/search-bar.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
32 changes: 16 additions & 16 deletions frontend/src/app/apps/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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',
Expand All @@ -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: {
Expand All @@ -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(),
]),
},
};
}
Expand Down
Loading

0 comments on commit ee071b3

Please sign in to comment.