diff --git a/app/integrations/page.tsx b/app/integrations/page.tsx index ae66c7e..cb7b8ed 100644 --- a/app/integrations/page.tsx +++ b/app/integrations/page.tsx @@ -1,13 +1,14 @@ 'use client' /** - * @file Integrations overview page with search, grouped by category. + * @file Integrations overview page with search, category filters, and grouped grid. * - * Displays all 50 integrations in a filterable grid. - * When the search query returns no results, a "Missing something?" card is shown. + * Displays all 75+ integrations in a filterable, searchable grid. + * Features: search with result count, category chips, popular section, + * keyboard shortcut (/ to focus search), and "Missing something?" card. */ -import { useState, useMemo } from 'react' +import { useState, useMemo, useRef, useEffect, useCallback } from 'react' import Link from 'next/link' import { motion, AnimatePresence } from 'framer-motion' import { ArrowRightIcon } from '@ciphera-net/ui' @@ -18,32 +19,75 @@ import { type IntegrationCategory, } from '@/lib/integrations' +// * IDs of popular integrations shown in the pinned "Popular" row +const POPULAR_IDS = [ + 'nextjs', 'react', 'wordpress', 'shopify', 'webflow', 'vue', 'astro', 'vercel', +] + export default function IntegrationsPage() { const [query, setQuery] = useState('') + const [activeCategory, setActiveCategory] = useState('all') + const searchRef = useRef(null) - // * Filter integrations by name, description, or category label - const filteredGroups = useMemo(() => { + // * Keyboard shortcut: "/" to focus search + useEffect(() => { + function handleKeyDown(e: KeyboardEvent) { + if ( + e.key === '/' && + !['INPUT', 'TEXTAREA', 'SELECT'].includes((e.target as HTMLElement).tagName) + ) { + e.preventDefault() + searchRef.current?.focus() + } + } + document.addEventListener('keydown', handleKeyDown) + return () => document.removeEventListener('keydown', handleKeyDown) + }, []) + + // * Filter integrations by search query + active category + const { filteredGroups, totalResults, popularIntegrations } = useMemo(() => { const q = query.toLowerCase().trim() + const isSearching = q.length > 0 + const isCategoryFiltered = activeCategory !== 'all' - const filtered = q - ? integrations.filter( - (i) => - i.name.toLowerCase().includes(q) || - i.description.toLowerCase().includes(q) || - categoryLabels[i.category].toLowerCase().includes(q), - ) - : integrations + let filtered = integrations - return categoryOrder + if (isSearching) { + filtered = filtered.filter( + (i) => + i.name.toLowerCase().includes(q) || + i.description.toLowerCase().includes(q) || + categoryLabels[i.category].toLowerCase().includes(q), + ) + } + + if (isCategoryFiltered) { + filtered = filtered.filter((i) => i.category === activeCategory) + } + + const groups = categoryOrder .map((cat) => ({ category: cat as IntegrationCategory, label: categoryLabels[cat], items: filtered.filter((i) => i.category === cat), })) .filter((g) => g.items.length > 0) - }, [query]) + + // * Only show popular row when not searching/filtering + const popular = + !isSearching && !isCategoryFiltered + ? POPULAR_IDS.map((id) => integrations.find((i) => i.id === id)).filter(Boolean) + : [] + + return { filteredGroups: groups, totalResults: filtered.length, popularIntegrations: popular } + }, [query, activeCategory]) const hasResults = filteredGroups.length > 0 + const isFiltering = query.length > 0 || activeCategory !== 'all' + + const handleCategoryClick = useCallback((cat: IntegrationCategory | 'all') => { + setActiveCategory(cat) + }, []) return (
@@ -62,16 +106,22 @@ export default function IntegrationsPage() { initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5 }} - className="text-center mb-12" + className="text-center mb-10" > -

- Integrations -

+ {/* * --- Title with count badge --- */} +
+

+ Integrations +

+ + {integrations.length}+ + +

- Connect Pulse with your favorite frameworks and platforms in minutes. + Connect Pulse with {integrations.length}+ frameworks and platforms in minutes.

- {/* * --- Search Input --- */} + {/* * --- Search Input with "/" hint --- */}
setQuery(e.target.value)} placeholder="Search integrations..." - className="w-full pl-12 pr-10 py-3 bg-white/70 dark:bg-neutral-900/70 backdrop-blur-sm border border-neutral-200 dark:border-neutral-800 rounded-xl text-neutral-900 dark:text-white placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-brand-orange/50 focus:border-brand-orange/50 transition-all" + className="w-full pl-12 pr-16 py-3 bg-white/70 dark:bg-neutral-900/70 backdrop-blur-sm border border-neutral-200 dark:border-neutral-800 rounded-xl text-neutral-900 dark:text-white placeholder:text-neutral-400 focus:outline-none focus:ring-2 focus:ring-brand-orange/50 focus:border-brand-orange/50 transition-all" /> - {query && ( + {query ? ( + ) : ( +
+ + / + +
)}
+ + {/* * --- Result count (shown when filtering) --- */} + {isFiltering && ( + + {totalResults} {totalResults === 1 ? 'integration' : 'integrations'} found + {query && <> for “{query}”} + + )} + + + {/* * --- Category Filter Chips --- */} + + + {categoryOrder.map((cat) => ( + + ))} @@ -118,6 +219,49 @@ export default function IntegrationsPage() { exit={{ opacity: 0 }} transition={{ duration: 0.2 }} > + {/* * --- Popular Integrations (pinned row) --- */} + {popularIntegrations.length > 0 && ( +
+ + + + + Popular + + +
+ {popularIntegrations.map((integration, i) => ( + + +
+ {integration!.icon} +
+ + {integration!.name} + + +
+ ))} +
+
+ )} + + {/* * --- Category Groups --- */} {filteredGroups.map((group) => (