> ## Documentation Index
> Fetch the complete documentation index at: https://docs.callers.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# App Center

> Connect 600+ apps to supercharge your campaigns with pre-built integrations

Callers integrates with **600+ apps** across 15 categories. Connect your favorite tools to automate pre-call enrichment, in-call actions, and post-call workflows.

<Note>
  To use an integration, go to **App Center** in your dashboard and authenticate
  the app. Once connected, it becomes available in all campaign stages.
</Note>

export const AppCenterFilter = () => {
  const [searchQuery, setSearchQuery] = React.useState('');
  const [activeCategory, setActiveCategory] = React.useState(null);
  const [viewMode, setViewMode] = React.useState('grid');
  const [integrations, setIntegrations] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const searchRef = React.useRef(null);
  const loadMoreRef = React.useRef(null);
  const [visibleCount, setVisibleCount] = React.useState(24);
  React.useEffect(() => {
    fetch('https://api.npoint.io/af256ab4318b3c120e5a').then(res => res.json()).then(data => {
      setIntegrations(data);
      setLoading(false);
    }).catch(err => {
      console.error('Failed to load integrations:', err);
      setLoading(false);
    });
  }, []);
  React.useEffect(() => {
    const handleKeyDown = e => {
      if (e.key === '/' && document.activeElement.tagName !== 'INPUT') {
        e.preventDefault();
        searchRef.current?.focus();
      }
    };
    document.addEventListener('keydown', handleKeyDown);
    return () => document.removeEventListener('keydown', handleKeyDown);
  }, []);
  const categoryLabels = {
    'AI': 'AI',
    'ANALYTICS_AND_REPORTING': 'Analytics & Reporting',
    'CORE_TOOLS': 'Core Tools',
    'EMAIL': 'Email',
    'E_COMMERCE_AND_PAYMENTS': 'E-Commerce & Payments',
    'HELPDESK_AND_CUSTOMER_SUPPORT': 'Helpdesk & Customer Support',
    'HR_AND_RECRUITMENT': 'HR & Recruitment',
    'MARKETING_AUTOMATION': 'Marketing Automation',
    'OTHERS': 'Others',
    'PRODUCTIVITY_AND_COLLABORATION': 'Productivity & Collaboration',
    'SALES_AND_CRM': 'Sales & CRM',
    'SCHEDULING_AND_CALENDAR': 'Scheduling & Calendar',
    'SOCIAL_MEDIA': 'Social Media',
    'TELEPHONY_AND_COMMUNICATION': 'Telephony & Communication',
    'WORKFLOW_AUTOMATION': 'Workflow Automation'
  };
  const categoryOrder = ['AI', 'SALES_AND_CRM', 'MARKETING_AUTOMATION', 'PRODUCTIVITY_AND_COLLABORATION', 'EMAIL', 'E_COMMERCE_AND_PAYMENTS', 'TELEPHONY_AND_COMMUNICATION', 'CORE_TOOLS', 'WORKFLOW_AUTOMATION', 'HR_AND_RECRUITMENT', 'SCHEDULING_AND_CALENDAR', 'HELPDESK_AND_CUSTOMER_SUPPORT', 'SOCIAL_MEDIA', 'ANALYTICS_AND_REPORTING', 'OTHERS'];
  const categoryCounts = React.useMemo(() => {
    const counts = {};
    integrations.forEach(i => {
      const cat = i.categories?.[0] || 'OTHERS';
      counts[cat] = (counts[cat] || 0) + 1;
    });
    return counts;
  }, [integrations]);
  const availableCategories = React.useMemo(() => {
    return Object.keys(categoryCounts).sort((a, b) => {
      if (a === 'OTHERS') return 1;
      if (b === 'OTHERS') return -1;
      const iA = categoryOrder.indexOf(a), iB = categoryOrder.indexOf(b);
      if (iA === -1 && iB === -1) return a.localeCompare(b);
      if (iA === -1) return 1;
      if (iB === -1) return -1;
      return iA - iB;
    });
  }, [categoryCounts]);
  const filtered = React.useMemo(() => {
    let result = integrations;
    if (activeCategory) {
      result = result.filter(i => (i.categories?.[0] || 'OTHERS') === activeCategory);
    }
    if (searchQuery.trim()) {
      const query = searchQuery.toLowerCase();
      result = result.filter(i => i.displayName.toLowerCase().includes(query));
    }
    return result;
  }, [searchQuery, activeCategory, integrations]);
  const sortedFiltered = React.useMemo(() => {
    return [...filtered].sort((a, b) => {
      const catA = a.categories?.[0] || 'OTHERS';
      const catB = b.categories?.[0] || 'OTHERS';
      const iA = categoryOrder.indexOf(catA);
      const iB = categoryOrder.indexOf(catB);
      if (iA === -1 && iB === -1) return catA.localeCompare(catB);
      if (iA === -1) return 1;
      if (iB === -1) return -1;
      if (iA !== iB) return iA - iB;
      return a.displayName.localeCompare(b.displayName);
    });
  }, [filtered]);
  const visibleIntegrations = React.useMemo(() => {
    return sortedFiltered.slice(0, visibleCount);
  }, [sortedFiltered, visibleCount]);
  const grouped = React.useMemo(() => {
    const groups = {};
    visibleIntegrations.forEach(i => {
      const cat = i.categories?.[0] || 'OTHERS';
      if (!groups[cat]) groups[cat] = [];
      groups[cat].push(i);
    });
    Object.keys(groups).forEach(cat => groups[cat].sort((a, b) => a.displayName.localeCompare(b.displayName)));
    return groups;
  }, [visibleIntegrations]);
  const sortedCategories = React.useMemo(() => {
    return Object.keys(grouped).sort((a, b) => {
      if (a === 'OTHERS') return 1;
      if (b === 'OTHERS') return -1;
      const iA = categoryOrder.indexOf(a), iB = categoryOrder.indexOf(b);
      if (iA === -1 && iB === -1) return a.localeCompare(b);
      if (iA === -1) return 1;
      if (iB === -1) return -1;
      return iA - iB;
    });
  }, [grouped]);
  React.useEffect(() => {
    const observer = new IntersectionObserver(entries => {
      if (entries[0].isIntersecting) {
        setVisibleCount(prev => Math.min(prev + 24, filtered.length));
      }
    }, {
      threshold: 0.1,
      rootMargin: "100px"
    });
    if (loadMoreRef.current) {
      observer.observe(loadMoreRef.current);
    }
    return () => observer.disconnect();
  }, [filtered.length]);
  React.useEffect(() => {
    setVisibleCount(24);
  }, [searchQuery, activeCategory]);
  const clearFilters = () => {
    setSearchQuery('');
    setActiveCategory(null);
  };
  if (loading) {
    return <div className="app-center-filter"><p>Loading integrations...</p></div>;
  }
  return <div className="app-center-filter">
<div className="filter-row"><div className="search-input-wrapper">
<input ref={searchRef} type="text" className="search-input" placeholder="Search integrations..." value={searchQuery} onChange={e => setSearchQuery(e.target.value)} />
{searchQuery && <button className="clear-button" onClick={() => setSearchQuery('')}>×</button>}</div><select className="category-dropdown" value={activeCategory || ''} onChange={e => setActiveCategory(e.target.value || null)}><option value="">All Categories ({integrations.length})</option>{availableCategories.map(cat => <option key={cat} value={cat}>{categoryLabels[cat] || cat} ({categoryCounts[cat]})</option>)}</select><div className="view-toggle"><button className={`view-btn ${viewMode === 'grid' ? 'active' : ''}`} onClick={() => setViewMode('grid')} title="Grid view"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="3" width="7" height="7" /><rect x="14" y="3" width="7" height="7" /><rect x="3" y="14" width="7" height="7" /><rect x="14" y="14" width="7" height="7" /></svg></button><button className={`view-btn ${viewMode === 'list' ? 'active' : ''}`} onClick={() => setViewMode('list')} title="List view"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><line x1="3" y1="6" x2="21" y2="6" /><line x1="3" y1="12" x2="21" y2="12" /><line x1="3" y1="18" x2="21" y2="18" /></svg></button></div></div>

{filtered.length === 0 ? <div className="no-results">
<p>No integrations found{searchQuery ? ' matching "' + searchQuery + '"' : ''}{activeCategory ? ' in ' + (categoryLabels[activeCategory] || activeCategory) : ''}</p>
<button onClick={clearFilters}>Clear filters</button>
</div> : sortedCategories.map(category => <div key={category} className="category-section">
<h2>{categoryLabels[category] || category}</h2>
<div className={`cards-grid ${viewMode}`}>
{grouped[category].map(integration => <div key={integration.id} className="integration-card-wrapper">
<div className="integration-card">
<img src={integration.logoUrl} alt={integration.displayName} loading="lazy" />
<div className="integration-text"><p className="integration-name">{integration.displayName}</p>
<span className="integration-description">{integration.description}</span></div>
</div>
</div>)}
</div>
</div>)}
{visibleCount < filtered.length && <div ref={loadMoreRef} className="load-more-sentinel">
<p>Loading more integrations...</p>
</div>}
</div>;
};

<AppCenterFilter />
