diff --git a/frontend/public/app-store-badge.svg b/frontend/public/app-store-badge.svg new file mode 100755 index 000000000..072b425a1 --- /dev/null +++ b/frontend/public/app-store-badge.svg @@ -0,0 +1,46 @@ + + Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/google-play-badge.png b/frontend/public/google-play-badge.png new file mode 100644 index 000000000..7a06997a5 Binary files /dev/null and b/frontend/public/google-play-badge.png differ diff --git a/frontend/public/omi_1.webp b/frontend/public/omi_1.webp new file mode 100644 index 000000000..3daae2799 Binary files /dev/null and b/frontend/public/omi_1.webp differ diff --git a/frontend/public/omi_2.webp b/frontend/public/omi_2.webp new file mode 100644 index 000000000..67a3cd142 Binary files /dev/null and b/frontend/public/omi_2.webp differ diff --git a/frontend/src/app/apps/[id]/page.tsx b/frontend/src/app/apps/[id]/page.tsx index 57c0b47b6..e1d8969ab 100644 --- a/frontend/src/app/apps/[id]/page.tsx +++ b/frontend/src/app/apps/[id]/page.tsx @@ -1,55 +1,144 @@ import envConfig from '@/src/constants/envConfig'; -import { - Star, - Download, - ArrowLeft, - Brain, - Cpu, - Bell, - Plug2, - MessageSquare, - Info, -} from 'lucide-react'; -import { Card, CardContent } from '@/src/components/ui/card'; -import { Button } from '@/src/components/ui/button'; import { Plugin, PluginStat } from '../components/types'; import { headers } from 'next/headers'; -import Link from 'next/link'; -import { cn } from '@/src/lib/utils'; import { CompactPluginCard } from '../components/plugin-card/compact'; +import { ScrollableCategoryNav } from '../components/scrollable-category-nav'; +import { CategoryBreadcrumb } from '../components/category-breadcrumb'; +import { AppStats } from '../components/app-stats'; +import { AppActionButton } from '../components/app-action-button'; +import { Calendar, User, FolderOpen, Puzzle } from 'lucide-react'; +import { Metadata, ResolvingMetadata } from 'next'; +import { ProductBanner } from '@/src/app/components/product-banner'; -// Helper functions from PluginCard -const getCapabilityColor = (capability: string): string => { - const colors: Record = { - 'ai-powered': 'bg-indigo-500/15 text-indigo-300', - memories: 'bg-rose-500/15 text-rose-300', - notification: 'bg-emerald-500/15 text-emerald-300', - integration: 'bg-sky-500/15 text-sky-300', - chat: 'bg-violet-500/15 text-violet-300', - }; - return colors[capability.toLowerCase()] ?? 'bg-gray-700/20 text-gray-300'; +type Props = { + params: { id: string }; }; -const formatCapabilityName = (capability: string): string => { - const nameMap: Record = { - memories: 'memories', - external_integration: 'integration', - proactive_notification: 'notification', - chat: 'chat', +export async function generateMetadata( + { params }: Props, + parent: ResolvingMetadata, +): Promise { + const response = await fetch( + `${envConfig.API_URL}/v1/approved-apps?include_reviews=true`, + ); + const plugins = (await response.json()) as Plugin[]; + const plugin = plugins.find((p) => p.id === params.id); + + if (!plugin) { + return { + title: 'App Not Found | Omi', + description: 'The requested app could not be found.', + }; + } + + 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'; + + return { + title: `${plugin.name} - ${categoryName} App | Omi`, + description: `${plugin.description} Available on Omi, the AI-powered wearable platform.`, + metadataBase: new URL('https://omi.me'), + alternates: { + canonical: canonicalUrl, + }, + openGraph: { + title: `${plugin.name} - ${categoryName} App`, + description: plugin.description, + images: [plugin.image], + url: canonicalUrl, + type: 'website', + siteName: 'Omi', + }, + twitter: { + card: 'summary_large_image', + title: `${plugin.name} - ${categoryName} App`, + description: plugin.description, + images: [plugin.image], + creator: '@omi', + site: '@omi', + }, + other: { + 'application-name': 'Omi', + 'apple-itunes-app': `app-id=6502156163`, + 'google-play-app': `app-id=com.friend.ios`, + }, }; - return nameMap[capability.toLowerCase()] ?? capability; -}; +} + +// Add a separate function to handle JSON-LD +export function generateStructuredData(plugin: Plugin, categoryName: string) { + 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 productUrl = 'https://www.omi.me/products/friend-dev-kit-2'; -const getCapabilityIcon = (capability: string) => { - const icons: Record = { - 'ai-powered': Brain, - memories: Cpu, - notification: Bell, - integration: Plug2, - chat: MessageSquare, + return { + __html: JSON.stringify([ + { + '@context': 'https://schema.org', + '@type': 'SoftwareApplication', + name: plugin.name, + description: plugin.description, + applicationCategory: categoryName, + operatingSystem: 'iOS, Android', + author: { + '@type': 'Person', + name: plugin.author, + }, + datePublished: plugin.created_at, + aggregateRating: { + '@type': 'AggregateRating', + ratingValue: plugin.rating_avg?.toFixed(1) || '0', + ratingCount: plugin.rating_count || 0, + bestRating: '5', + worstRating: '1', + }, + applicationSuite: 'Omi', + requiresSubscription: false, + installUrl: canonicalUrl, + interactionStatistic: { + '@type': 'InteractionCounter', + interactionType: 'https://schema.org/InstallAction', + userInteractionCount: plugin.installs, + }, + }, + { + '@context': 'https://schema.org', + '@type': 'Product', + name: 'OMI Necklace', + description: 'AI-powered wearable necklace. Real-time AI voice assistant.', + brand: { + '@type': 'Brand', + name: 'OMI', + }, + offers: { + '@type': 'Offer', + price: '69.99', + priceCurrency: 'USD', + availability: 'https://schema.org/InStock', + url: productUrl, + priceValidUntil: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000) + .toISOString() + .split('T')[0], // Valid for 1 year + }, + additionalProperty: [ + { + '@type': 'PropertyValue', + name: 'App Store', + value: appStoreUrl, + }, + { + '@type': 'PropertyValue', + name: 'Play Store', + value: playStoreUrl, + }, + ], + }, + ]), }; - return icons[capability.toLowerCase()] ?? Info; -}; +} // Helper function to format category name const formatCategoryName = (category: string): string => { @@ -71,6 +160,15 @@ function getPlatformLink(userAgent: string) { : 'https://omi.me'; } +// Helper function to format date +function formatDate(dateString: string): string { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }); +} + export default async function PluginDetailView({ params }: { params: { id: string } }) { const response = await fetch( `${envConfig.API_URL}/v1/approved-apps?include_reviews=true`, @@ -100,113 +198,187 @@ export default async function PluginDetailView({ params }: { params: { id: strin const categoryName = formatCategoryName(plugin.category); return ( -
- {/* Main Content */} -
- {/* Navigation */} -