@@ -6,54 +6,132 @@ import { blockTypeToIconMap } from '@/app/(landing)/integrations/data/icon-mappi
66import type { Integration } from '@/app/(landing)/integrations/data/types'
77import { IntegrationCard } from './integration-card'
88
9+ const CATEGORY_LABELS : Record < string , string > = {
10+ ai : 'AI' ,
11+ analytics : 'Analytics' ,
12+ automation : 'Automation' ,
13+ communication : 'Communication' ,
14+ crm : 'CRM' ,
15+ 'customer-support' : 'Customer Support' ,
16+ databases : 'Databases' ,
17+ design : 'Design' ,
18+ 'developer-tools' : 'Developer Tools' ,
19+ documents : 'Documents' ,
20+ ecommerce : 'E-commerce' ,
21+ email : 'Email' ,
22+ 'file-storage' : 'File Storage' ,
23+ hr : 'HR' ,
24+ media : 'Media' ,
25+ productivity : 'Productivity' ,
26+ 'sales-intelligence' : 'Sales Intelligence' ,
27+ search : 'Search' ,
28+ security : 'Security' ,
29+ social : 'Social' ,
30+ other : 'Other' ,
31+ } as const
32+
933interface IntegrationGridProps {
1034 integrations : Integration [ ]
1135}
1236
1337export function IntegrationGrid ( { integrations } : IntegrationGridProps ) {
1438 const [ query , setQuery ] = useState ( '' )
39+ const [ activeCategory , setActiveCategory ] = useState < string | null > ( null )
40+
41+ const availableCategories = useMemo ( ( ) => {
42+ const counts = new Map < string , number > ( )
43+ for ( const i of integrations ) {
44+ if ( i . integrationType ) {
45+ counts . set ( i . integrationType , ( counts . get ( i . integrationType ) || 0 ) + 1 )
46+ }
47+ }
48+ return Array . from ( counts . entries ( ) )
49+ . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] )
50+ . map ( ( [ key ] ) => key )
51+ } , [ integrations ] )
1552
1653 const filtered = useMemo ( ( ) => {
54+ let results = integrations
55+
56+ if ( activeCategory ) {
57+ results = results . filter ( ( i ) => i . integrationType === activeCategory )
58+ }
59+
1760 const q = query . trim ( ) . toLowerCase ( )
18- if ( ! q ) return integrations
19- return integrations . filter (
20- ( i ) =>
21- i . name . toLowerCase ( ) . includes ( q ) ||
22- i . description . toLowerCase ( ) . includes ( q ) ||
23- i . operations . some (
24- ( op ) => op . name . toLowerCase ( ) . includes ( q ) || op . description . toLowerCase ( ) . includes ( q )
25- ) ||
26- i . triggers . some ( ( t ) => t . name . toLowerCase ( ) . includes ( q ) )
27- )
28- } , [ integrations , query ] )
61+ if ( q ) {
62+ results = results . filter (
63+ ( i ) =>
64+ i . name . toLowerCase ( ) . includes ( q ) ||
65+ i . description . toLowerCase ( ) . includes ( q ) ||
66+ i . operations . some (
67+ ( op ) => op . name . toLowerCase ( ) . includes ( q ) || op . description . toLowerCase ( ) . includes ( q )
68+ ) ||
69+ i . triggers . some ( ( t ) => t . name . toLowerCase ( ) . includes ( q ) )
70+ )
71+ }
72+
73+ return results
74+ } , [ integrations , query , activeCategory ] )
2975
3076 return (
3177 < div >
32- < div className = 'relative mb-8 max-w-[480px]' >
33- < svg
34- aria-hidden = 'true'
35- className = '-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[#555]'
36- fill = 'none'
37- stroke = 'currentColor'
38- strokeWidth = { 2 }
39- viewBox = '0 0 24 24'
78+ < div className = 'mb-6 flex flex-col gap-4 sm:flex-row sm:items-center' >
79+ < div className = 'relative max-w-[480px] flex-1' >
80+ < svg
81+ aria-hidden = 'true'
82+ className = '-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[#555]'
83+ fill = 'none'
84+ stroke = 'currentColor'
85+ strokeWidth = { 2 }
86+ viewBox = '0 0 24 24'
87+ >
88+ < circle cx = { 11 } cy = { 11 } r = { 8 } />
89+ < path d = 'm21 21-4.35-4.35' />
90+ </ svg >
91+ < Input
92+ type = 'search'
93+ placeholder = 'Search integrations, tools, or triggers…'
94+ value = { query }
95+ onChange = { ( e ) => setQuery ( e . target . value ) }
96+ className = 'pl-9'
97+ aria-label = 'Search integrations'
98+ />
99+ </ div >
100+ </ div >
101+
102+ < div className = 'mb-8 flex flex-wrap gap-2' >
103+ < button
104+ type = 'button'
105+ onClick = { ( ) => setActiveCategory ( null ) }
106+ className = { `rounded-md border px-3 py-1 text-[12px] transition-colors ${
107+ activeCategory === null
108+ ? 'border-[#555] bg-[#333] text-[#ECECEC]'
109+ : 'border-[#2A2A2A] bg-transparent text-[#999] hover:border-[#3d3d3d] hover:text-[#ECECEC]'
110+ } `}
40111 >
41- < circle cx = { 11 } cy = { 11 } r = { 8 } />
42- < path d = 'm21 21-4.35-4.35' />
43- </ svg >
44- < Input
45- type = 'search'
46- placeholder = 'Search integrations, tools, or triggers…'
47- value = { query }
48- onChange = { ( e ) => setQuery ( e . target . value ) }
49- className = 'pl-9'
50- aria-label = 'Search integrations'
51- />
112+ All
113+ </ button >
114+ { availableCategories . map ( ( cat ) => (
115+ < button
116+ key = { cat }
117+ type = 'button'
118+ onClick = { ( ) => setActiveCategory ( activeCategory === cat ? null : cat ) }
119+ className = { `rounded-md border px-3 py-1 text-[12px] transition-colors ${
120+ activeCategory === cat
121+ ? 'border-[#555] bg-[#333] text-[#ECECEC]'
122+ : 'border-[#2A2A2A] bg-transparent text-[#999] hover:border-[#3d3d3d] hover:text-[#ECECEC]'
123+ } `}
124+ >
125+ { CATEGORY_LABELS [ cat ] || cat }
126+ </ button >
127+ ) ) }
52128 </ div >
53129
54130 { filtered . length === 0 ? (
55131 < p className = 'py-12 text-center text-[#555] text-[15px]' >
56- No integrations found for “{ query } ”
132+ No integrations found
133+ { query ? < > for “{ query } ”</ > : null }
134+ { activeCategory ? < > in { CATEGORY_LABELS [ activeCategory ] || activeCategory } </ > : null }
57135 </ p >
58136 ) : (
59137 < div className = 'grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4' >
0 commit comments