'use client' import { useMemo } from 'react' import { ChipInput, Search } from '@sim/emcn' import { debounce, useQueryStates } from 'nuqs' import { blockTypeToIconMap, formatIntegrationType, type Integration } from '@/lib/integrations' import { IntegrationRow } from '@/app/(landing)/integrations/components/integration-card' import { integrationsParsers, integrationsUrlKeys, } from '@/app/(landing)/integrations/search-params' /** Debounce window for writing the search term to the URL (filtering is instant). */ const SEARCH_DEBOUNCE_MS = 300 const PILL_BASE = 'rounded-[5px] border border-[var(--border-1)] px-[9px] py-0.5 text-small text-[var(--text-primary)] transition-colors' as const const PILL_ACTIVE = 'bg-[var(--surface-active)]' as const const PILL_INACTIVE = 'hover:bg-[var(--surface-hover)]' as const interface IntegrationGridProps { integrations: readonly Integration[] } export function IntegrationGrid({ integrations }: IntegrationGridProps) { const [{ q: query, category }, setParams] = useQueryStates( integrationsParsers, integrationsUrlKeys ) const activeCategory = category || null // Category facets and a per-integration lowercased search index, derived once // from the (stable) integration list instead of rebuilt on every keystroke. // The index keeps each searchable field as its own entry so matching stays // identical to a per-field `includes` (no cross-field boundary matches). const { availableCategories, searchIndex } = useMemo(() => { const counts = new Map() const searchIndex = new Map() for (const i of integrations) { if (i.integrationType) { counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1) } searchIndex.set(i.type, [ i.name.toLowerCase(), i.description.toLowerCase(), ...i.operations.flatMap((op) => [op.name.toLowerCase(), op.description.toLowerCase()]), ...i.triggers.map((t) => t.name.toLowerCase()), ]) } return { availableCategories: Array.from(counts.entries()) .sort((a, b) => b[1] - a[1]) .map(([key]) => key), searchIndex, } }, [integrations]) const q = query.trim().toLowerCase() const filtered = useMemo( () => integrations.filter((i) => { if (activeCategory && i.integrationType !== activeCategory) return false if (!q) return true return searchIndex.get(i.type)?.some((field) => field.includes(q)) ?? false }), [integrations, searchIndex, q, activeCategory] ) return (
setParams({ q: e.target.value }, { limitUrlUpdates: debounce(SEARCH_DEBOUNCE_MS) }) } aria-label='Search integrations' />
{availableCategories.map((cat) => ( ))}
{filtered.length === 0 ? (

No integrations found {query ? <> for “{query}” : null} {activeCategory ? <> in {formatIntegrationType(activeCategory)} : null}

) : (
{filtered.map((integration) => ( ))}
)}
) }