Marketplace.constants.tsx167 lines · main
1import {
2 BadgeCheck,
3 BarChart3,
4 Boxes,
5 Cable,
6 Cpu,
7 CreditCard,
8 Database,
9 Fingerprint,
10 KeyRound,
11 Mail,
12 MessageSquare,
13 MousePointerClick,
14 Package2,
15 Plug,
16 Server,
17 ShieldCheck,
18 Users,
19 Webhook,
20 Wrench,
21 Zap,
22 type LucideIcon,
23} from 'lucide-react'
24import type { ReactNode } from 'react'
25import { Badge, cn, IconPartners } from 'ui'
26
27import type {
28 IntegrationDefinition,
29 MarketplaceSource,
30} from '@/components/interfaces/Integrations/Landing/Integrations.constants'
31
32export type { MarketplaceSource } from '@/components/interfaces/Integrations/Landing/Integrations.constants'
33
34// Launch-partner pin list for the featured rail. Order here is the order
35// shown. Stripe is the only first-party template in the set (id matches the
36// static catalogue); the rest are marketplaceDB slugs and only render if the
37// listing is published.
38export const FEATURED_INTEGRATION_IDS = [
39 'grafana',
40 'stripe_sync_engine',
41 'aikido',
42 'aikido-security',
43 'doppler',
44 'cipherstash',
45] as const
46
47export type MarketplaceIntegrationType = 'oauth' | 'postgres_extension' | 'template' | 'wrapper'
48
49export const INTEGRATION_TYPES: Array<{
50 key: MarketplaceIntegrationType
51 label: string
52 icon: LucideIcon
53}> = [
54 { key: 'oauth', label: 'OAuth App', icon: Plug },
55 { key: 'postgres_extension', label: 'Postgres Module', icon: Database },
56 { key: 'template', label: 'Template', icon: Package2 },
57 { key: 'wrapper', label: 'Wrapper', icon: Cable },
58]
59
60// Lucide icon per marketplace category slug. New categories fall back to a
61// neutral icon (Boxes) so nothing breaks when the marketplaceDB is updated.
62export const CATEGORY_ICONS: Record<string, LucideIcon> = {
63 observability: BarChart3,
64 security: ShieldCheck,
65 billing: CreditCard,
66 secrets: KeyRound,
67 email: Mail,
68 wrappers: Database,
69 ai: Cpu,
70 ai_vectors: Cpu,
71 storage: Package2,
72 postgres_extension: Database,
73 wrapper: Cable,
74 devtools: Wrench,
75 auth: Fingerprint,
76 'low-code': MousePointerClick,
77 'data-platform': Server,
78 api: Webhook,
79 'caching-offline-first': Zap,
80 messaging: MessageSquare,
81}
82
83export const FEATURED_CATEGORIES: Array<{ slug: string; name: string }> = [
84 { slug: 'observability', name: 'Observability' },
85 { slug: 'security', name: 'Security' },
86 { slug: 'billing', name: 'Billing' },
87 { slug: 'devtools', name: 'DevTools' },
88 { slug: 'ai_vectors', name: 'AI & Vectors' },
89 { slug: 'storage', name: 'Storage' },
90]
91
92// Categories excluded from the filter dropdown — they either overlap with
93// dedicated filter groups (type/source) or are unused in the current catalogue.
94export const EXCLUDED_CATEGORY_SLUGS = new Set([
95 'agencies',
96 'foreign-data-wrapper',
97 'app-templates',
98])
99
100export const getCategoryIcon = (slug: string | null | undefined): LucideIcon => {
101 if (!slug) return Boxes
102 return CATEGORY_ICONS[slug] ?? Boxes
103}
104
105export const formatCategoryLabel = (
106 slug: string | null | undefined,
107 categoryOptions?: Array<{ slug: string; name: string }>
108): string => {
109 if (!slug) return ''
110 const pinned = FEATURED_CATEGORIES.find((c) => c.slug === slug)
111 if (pinned) return pinned.name
112 const remote = categoryOptions?.find((c) => c.slug === slug)
113 if (remote) return remote.name
114 return slug
115 .split(/[-_]/)
116 .filter(Boolean)
117 .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
118 .join(' ')
119}
120
121export const getMarketplaceType = (
122 integration: IntegrationDefinition
123): MarketplaceIntegrationType => {
124 if (integration.type === 'wrapper') return 'wrapper'
125 if (integration.type === 'postgres_extension') return 'postgres_extension'
126 if (integration.id === 'stripe_sync_engine') return 'template'
127 return 'oauth'
128}
129
130export const getMarketplaceTypeLabel = (type: MarketplaceIntegrationType): string =>
131 INTEGRATION_TYPES.find((t) => t.key === type)?.label ?? type
132
133export const MARKETPLACE_SOURCES: Array<{
134 key: MarketplaceSource
135 label: string
136 icon: LucideIcon | ((props: { className?: string }) => ReactNode)
137}> = [
138 { key: 'Official', label: 'Official', icon: BadgeCheck },
139 { key: 'Partner', label: 'Partner', icon: IconPartners },
140 { key: 'Community', label: 'Community', icon: Users },
141]
142
143export const getMarketplaceSource = (integration: IntegrationDefinition): MarketplaceSource => {
144 if (integration.source) return integration.source
145 return integration.listingId ? 'Partner' : 'Official'
146}
147
148export const MarketplaceSourceBadge = ({
149 source,
150 className,
151}: {
152 source: MarketplaceSource
153 className?: string
154}) => {
155 switch (source) {
156 case 'Partner':
157 return (
158 <Badge className={cn('border-foreground-lighter/50', className)}>
159 <IconPartners size={10} /> Partner
160 </Badge>
161 )
162 case 'Community':
163 return <Badge className={className}>Community</Badge>
164 case 'Official':
165 return <Badge className={className}>Official</Badge>
166 }
167}