diff --git a/packages/backend/src/adapters/catalog.ts b/packages/backend/src/adapters/catalog.ts index e1eb7de..ebe823b 100644 --- a/packages/backend/src/adapters/catalog.ts +++ b/packages/backend/src/adapters/catalog.ts @@ -180,6 +180,9 @@ export interface AdapterMeta { docsUrl: string; requiredEnvVars: string[]; toolCount: number; + /** Surfaced on cards so the UI can render an auth-type chip without an + * extra round-trip to /api/adapters/:slug. */ + authType?: string; /** When true, surfaced in the marketing site's "Featured" rail. */ featured?: boolean; /** Higher = ranked earlier in catalog listings. Default 0. */ @@ -422,6 +425,7 @@ export function listAdapters(): AdapterMeta[] { docsUrl: adapter.docsUrl, requiredEnvVars: adapter.requiredEnvVars, toolCount: adapter.tools.length, + authType: adapter.connector.authType, featured: adapter.featured, priority: adapter.priority, })); diff --git a/packages/frontend/src/app/connectors/store/page.tsx b/packages/frontend/src/app/connectors/store/page.tsx index c079783..c6ff851 100644 --- a/packages/frontend/src/app/connectors/store/page.tsx +++ b/packages/frontend/src/app/connectors/store/page.tsx @@ -17,6 +17,78 @@ const REGION_LABELS: Record = { intl: 'International', }; +const REGION_FLAGS: Record = { + de: '๐Ÿ‡ฉ๐Ÿ‡ช', + eu: '๐Ÿ‡ช๐Ÿ‡บ', + global: '๐ŸŒ', + intl: '๐ŸŒ', + uk: '๐Ÿ‡ฌ๐Ÿ‡ง', + gb: '๐Ÿ‡ฌ๐Ÿ‡ง', + in: '๐Ÿ‡ฎ๐Ÿ‡ณ', + br: '๐Ÿ‡ง๐Ÿ‡ท', + ng: '๐Ÿ‡ณ๐Ÿ‡ฌ', + jp: '๐Ÿ‡ฏ๐Ÿ‡ต', +}; + +/* Deterministic colour palette for the monogram fallback when no SVG exists. + Mirrors lib/adapters.ts on the marketing site for visual consistency. */ +const MONOGRAM_PALETTE = [ + '#2563eb', '#7c3aed', '#0ea5e9', '#10b981', '#f59e0b', + '#ef4444', '#ec4899', '#14b8a6', '#6366f1', '#84cc16', + '#0891b2', '#a855f7', '#f97316', '#06b6d4', '#22c55e', +]; + +function monogramOf(name: string): string { + const stripped = name.replace(/[().]/g, '').trim(); + const parts = stripped.split(/\s+/); + if (parts.length >= 2) return (parts[0][0] + parts[1][0]).toUpperCase(); + return stripped.slice(0, 2).toUpperCase(); +} + +function brandColor(slug: string): string { + let h = 0; + for (let i = 0; i < slug.length; i++) h = (h * 31 + slug.charCodeAt(i)) >>> 0; + return MONOGRAM_PALETTE[h % MONOGRAM_PALETTE.length]; +} + +/* Brand logo or coloured monogram fallback. Matches the marketing-site + Marketplace card visual exactly so the in-app store feels like the same + product surface. */ +function BrandTile({ adapter, size = 44 }: { adapter: AdapterItem; size?: number }) { + const [failed, setFailed] = useState(false); + if (adapter.icon && !failed) { + return ( +
+ {adapter.name} setFailed(true)} + /> +
+ ); + } + return ( +
= 56 ? 22 : 14, + }} + > + {monogramOf(adapter.name)} +
+ ); +} + const CATEGORY_LABELS: Record = { logistics: 'Logistics', finance: 'Finance', @@ -54,7 +126,10 @@ const AUTH_LABELS: Record = { BEARER_TOKEN: 'Bearer Token', OAUTH2: 'OAuth 2.0', BASIC: 'Basic Auth', - NONE: 'None (Public API)', + BASIC_AUTH: 'Basic Auth', + QUERY_AUTH: 'Query Param Auth', + LOGIN_TOKEN: 'Login Token', + NONE: 'Public API', }; interface AdapterItem { @@ -67,6 +142,7 @@ interface AdapterItem { docsUrl: string; requiredEnvVars: string[]; toolCount: number; + authType?: string; } interface AdapterDetail extends AdapterItem { @@ -301,62 +377,103 @@ function AdapterStoreContent() { ) : (
- {filtered.map((adapter) => ( -
-
-

{adapter.name}

-
- {adapter.region && ( - - {REGION_LABELS[adapter.region] || adapter.region} - - )} + {filtered.map((adapter) => { + const isPublic = adapter.authType === 'NONE'; + const isImporting = importing === adapter.slug; + /* log-ish 1..10 segment scale, same as the marketing-site card */ + const fillCount = Math.max( + 1, + Math.min(10, Math.round(Math.log2(adapter.toolCount + 1) * 2.2)), + ); + return ( +
+
+ +
+
+ + {adapter.name} + + + {REGION_FLAGS[adapter.region] || '๐ŸŒ'} + +
+
+ + {CATEGORY_LABELS[adapter.category] || adapter.category} + + ยท + {REGION_LABELS[adapter.region] || adapter.region} +
+
-
-

- {adapter.description} -

+

+ {adapter.description} +

-
-
- {adapter.category && ( - {CATEGORY_LABELS[adapter.category] || adapter.category} - )} - {adapter.toolCount} tool{adapter.toolCount !== 1 ? 's' : ''} +
+
+ + {adapter.toolCount} + + tool{adapter.toolCount !== 1 ? 's' : ''} + + {Array.from({ length: 10 }, (_, i) => ( + + ))} + +
+
+ {adapter.authType && ( + + {isPublic ? : } + {AUTH_LABELS[adapter.authType] || adapter.authType} + + )} + {adapter.docsUrl && ( + + + + )} + +
- - -
- - {adapter.docsUrl && ( - - API Documentation - - )} -
- ))} + + ); + })}
)} @@ -509,9 +626,36 @@ function CloseIcon() { function LockIcon() { return ( - + ); } + +function SparklesIcon() { + return ( + + + + ); +} + +function ExternalLinkIcon() { + return ( + + + + + + ); +} + +function ArrowRightIcon() { + return ( + + + + + ); +}