MarketplaceIndex.tsx292 lines · main
1// @ts-nocheck
2import { useQuery } from '@tanstack/react-query'
3import { parseAsString, parseAsStringEnum, useQueryState } from 'nuqs'
4import { useMemo } from 'react'
5import { Button, Card, ShadowScrollArea, Table, TableBody, TableHeader } from 'ui'
6import {
7 EmptyStatePresentational,
8 PageContainer,
9 PageHeader,
10 PageHeaderDescription,
11 PageHeaderMeta,
12 PageHeaderSummary,
13 PageHeaderTitle,
14} from 'ui-patterns'
15
16import {
17 EXCLUDED_CATEGORY_SLUGS,
18 FEATURED_INTEGRATION_IDS,
19 formatCategoryLabel,
20 getMarketplaceSource,
21 getMarketplaceType,
22 INTEGRATION_TYPES,
23 MARKETPLACE_SOURCES,
24 type MarketplaceIntegrationType,
25 type MarketplaceSource,
26} from './Marketplace.constants'
27import { MarketplaceCard } from './MarketplaceCard'
28import { MarketplaceFeaturedHero } from './MarketplaceFeaturedHero'
29import { MarketplaceFilterBar, type ViewMode } from './MarketplaceFilterBar'
30import { MarketplaceListHeader, MarketplaceListRow } from './MarketplaceListRow'
31import { IntegrationLoadingCard } from '@/components/interfaces/Integrations/Landing/IntegrationCard'
32import { useAvailableIntegrations } from '@/components/interfaces/Integrations/Landing/useAvailableIntegrations'
33import { useInstalledIntegrations } from '@/components/interfaces/Integrations/Landing/useInstalledIntegrations'
34import { AlertError } from '@/components/ui/AlertError'
35import { DocsButton } from '@/components/ui/DocsButton'
36import { useMarketplaceCategoriesQuery } from '@/data/marketplace/integration-categories-query'
37import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
38import { DOCS_URL } from '@/lib/constants'
39
40const MARKETPLACE_VIEW_MODE_STORAGE_KEY = 'briven.marketplace.viewMode'
41
42export const MarketplaceIndex = () => {
43 const [search, setSearch] = useQueryState(
44 'search',
45 parseAsString.withDefault('').withOptions({ clearOnDefault: true })
46 )
47 const [category, setCategory] = useQueryState(
48 'category',
49 parseAsString.withOptions({ clearOnDefault: true })
50 )
51 const [type, setType] = useQueryState(
52 'type',
53 parseAsStringEnum<MarketplaceIntegrationType>([
54 'oauth',
55 'postgres_extension',
56 'template',
57 'wrapper',
58 ]).withOptions({ clearOnDefault: true })
59 )
60 const [source, setSource] = useQueryState(
61 'source',
62 parseAsStringEnum<MarketplaceSource>(['Official', 'Partner', 'Community']).withOptions({
63 clearOnDefault: true,
64 })
65 )
66
67 const [viewMode, setViewMode] = useLocalStorageQuery<ViewMode>(
68 MARKETPLACE_VIEW_MODE_STORAGE_KEY,
69 'grid'
70 )
71
72 const {
73 data: availableIntegrations,
74 error,
75 isPending: isLoadingAvailable,
76 isError,
77 isSuccess: isSuccessAvailable,
78 } = useAvailableIntegrations()
79
80 const {
81 installedIntegrations,
82 isLoading: isLoadingInstalled,
83 isSuccess: isSuccessInstalled,
84 } = useInstalledIntegrations()
85
86 const installedIds = installedIntegrations.map((i) => i.id)
87 const isLoading = isLoadingAvailable || isLoadingInstalled
88 const isSuccess = isSuccessAvailable && isSuccessInstalled
89
90 const { data: marketplaceCategories = [] } = useQuery(useMarketplaceCategoriesQuery())
91
92 const categoryOptions = useMemo(
93 () =>
94 marketplaceCategories
95 .filter(
96 (c): c is { slug: string; name: string } & typeof c =>
97 !!c.slug && !!c.name && !EXCLUDED_CATEGORY_SLUGS.has(c.slug)
98 )
99 .map((c) => ({ slug: c.slug, name: c.name })),
100 [marketplaceCategories]
101 )
102
103 const typeCounts = useMemo(() => {
104 const all = availableIntegrations ?? []
105 return Object.fromEntries(
106 INTEGRATION_TYPES.map(({ key }) => [
107 key,
108 all.filter((i) => getMarketplaceType(i) === key).length,
109 ])
110 ) as Record<MarketplaceIntegrationType, number>
111 }, [availableIntegrations])
112
113 const sourceCounts = useMemo(() => {
114 const all = availableIntegrations ?? []
115 return Object.fromEntries(
116 MARKETPLACE_SOURCES.map(({ key }) => [
117 key,
118 all.filter((i) => getMarketplaceSource(i) === key).length,
119 ])
120 ) as Record<MarketplaceSource, number>
121 }, [availableIntegrations])
122
123 const categoryCounts = useMemo(() => {
124 const all = availableIntegrations ?? []
125 const counts: Record<string, number> = {}
126 for (const { slug } of categoryOptions) {
127 counts[slug] = all.filter((i) => i.categories?.includes(slug)).length
128 }
129 return counts
130 }, [availableIntegrations, categoryOptions])
131
132 const hasActiveFilter = !!(category || type || source)
133 const hasSearchOrFilter = hasActiveFilter || search.length > 0
134
135 const filtered = useMemo(() => {
136 let result = availableIntegrations ?? []
137
138 if (category) {
139 result = result.filter((i) => i.categories?.includes(category))
140 }
141 if (type) {
142 result = result.filter((i) => getMarketplaceType(i) === type)
143 }
144 if (source) {
145 result = result.filter((i) => getMarketplaceSource(i) === source)
146 }
147 if (search.length > 0) {
148 const needle = search.toLowerCase()
149 result = result.filter((i) => i.name.toLowerCase().includes(needle))
150 }
151
152 return [...result].sort((a, b) => {
153 const aInstalled = installedIds.includes(a.id)
154 const bInstalled = installedIds.includes(b.id)
155 if (aInstalled && !bInstalled) return -1
156 if (!aInstalled && bInstalled) return 1
157 return a.name.localeCompare(b.name)
158 })
159 }, [availableIntegrations, category, type, source, search, installedIds])
160
161 const featured = useMemo(() => {
162 if (hasSearchOrFilter) return []
163 const byId = new Map((availableIntegrations ?? []).map((i) => [i.id, i]))
164 return FEATURED_INTEGRATION_IDS.map((id) => byId.get(id)).filter(
165 (i): i is NonNullable<typeof i> => !!i
166 )
167 }, [availableIntegrations, hasSearchOrFilter])
168
169 const clearAll = () => {
170 setCategory(null)
171 setType(null)
172 setSource(null)
173 setSearch('')
174 }
175
176 const activeFilters = [category, type, source].filter(Boolean)
177 const pageTitle = useMemo(() => {
178 if (activeFilters.length !== 1) return 'Integrations Marketplace'
179 if (category)
180 return `Integrations Marketplace: ${formatCategoryLabel(category, categoryOptions)}`
181 if (type)
182 return `Integrations Marketplace: ${INTEGRATION_TYPES.find((t) => t.key === type)?.label ?? type}s`
183 if (source)
184 return `Integrations Marketplace: ${MARKETPLACE_SOURCES.find((s) => s.key === source)?.label ?? source}`
185 return 'Integrations Marketplace'
186 }, [activeFilters.length, category, type, source, categoryOptions])
187
188 return (
189 <>
190 <PageHeader size="large">
191 <PageHeaderMeta>
192 <PageHeaderSummary>
193 <PageHeaderTitle>{pageTitle}</PageHeaderTitle>
194 <PageHeaderDescription>
195 Explore native and third-party integrations to add functionality to your Briven
196 project.
197 </PageHeaderDescription>
198 </PageHeaderSummary>
199 <div className="flex shrink-0 items-center gap-2">
200 <DocsButton href={`${DOCS_URL}/guides/integrations/briven-marketplace`} />
201 </div>
202 </PageHeaderMeta>
203 </PageHeader>
204
205 <PageContainer size="large" className="flex flex-col gap-4 py-6">
206 {isLoading && (
207 <div className="grid gap-4 xl:grid-cols-3 2xl:grid-cols-4">
208 {Array.from({ length: 8 }).map((_, idx) => (
209 <IntegrationLoadingCard key={`marketplace-loading-${idx}`} />
210 ))}
211 </div>
212 )}
213
214 {isError && (
215 <AlertError subject="Failed to retrieve available integrations" error={error} />
216 )}
217
218 {isSuccess && (
219 <>
220 {featured.length > 0 && (
221 <MarketplaceFeaturedHero
222 integrations={featured}
223 installedIds={installedIds}
224 categoryOptions={categoryOptions}
225 />
226 )}
227
228 <MarketplaceFilterBar
229 resultCount={filtered.length}
230 search={search}
231 onSearchChange={(v) => setSearch(v)}
232 category={category}
233 onCategoryChange={(v) => setCategory(v)}
234 categoryOptions={categoryOptions}
235 categoryCounts={categoryCounts}
236 type={type}
237 onTypeChange={(v) => setType(v)}
238 typeCounts={typeCounts}
239 source={source}
240 onSourceChange={(v) => setSource(v)}
241 sourceCounts={sourceCounts}
242 viewMode={viewMode}
243 onViewModeChange={(v) => setViewMode(v)}
244 hasActiveFilter={hasActiveFilter}
245 onClearFilters={clearAll}
246 />
247
248 {filtered.length === 0 && (
249 <EmptyStatePresentational title="No results found">
250 <Button type="default" onClick={clearAll}>
251 Clear filters
252 </Button>
253 </EmptyStatePresentational>
254 )}
255
256 {filtered.length > 0 &&
257 (viewMode === 'grid' ? (
258 <div className="grid @lg:grid-cols-2 gap-3 @4xl:grid-cols-3">
259 {filtered.map((integration) => (
260 <MarketplaceCard
261 key={integration.id}
262 integration={integration}
263 isInstalled={installedIds.includes(integration.id)}
264 />
265 ))}
266 </div>
267 ) : (
268 <Card>
269 <ShadowScrollArea>
270 <Table>
271 <TableHeader>
272 <MarketplaceListHeader />
273 </TableHeader>
274 <TableBody>
275 {filtered.map((integration) => (
276 <MarketplaceListRow
277 key={integration.id}
278 integration={integration}
279 isInstalled={installedIds.includes(integration.id)}
280 />
281 ))}
282 </TableBody>
283 </Table>
284 </ShadowScrollArea>
285 </Card>
286 ))}
287 </>
288 )}
289 </PageContainer>
290 </>
291 )
292}