@geenius/adapters 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/.changeset/config.json +11 -0
  2. package/.github/CODEOWNERS +1 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +16 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +11 -0
  5. package/.github/PULL_REQUEST_TEMPLATE.md +10 -0
  6. package/.github/dependabot.yml +11 -0
  7. package/.github/workflows/ci.yml +23 -0
  8. package/.github/workflows/release.yml +29 -0
  9. package/.nvmrc +1 -0
  10. package/.project/ACCOUNT.yaml +4 -0
  11. package/.project/IDEAS.yaml +7 -0
  12. package/.project/PROJECT.yaml +11 -0
  13. package/.project/ROADMAP.yaml +15 -0
  14. package/CHANGELOG.md +11 -0
  15. package/CODE_OF_CONDUCT.md +16 -0
  16. package/CONTRIBUTING.md +26 -0
  17. package/LICENSE +21 -0
  18. package/README.md +202 -0
  19. package/SECURITY.md +15 -0
  20. package/SUPPORT.md +8 -0
  21. package/package.json +51 -0
  22. package/packages/convex/README.md +64 -0
  23. package/packages/convex/package.json +42 -0
  24. package/packages/convex/src/adapter.ts +39 -0
  25. package/packages/convex/src/index.ts +19 -0
  26. package/packages/convex/src/mutations.ts +142 -0
  27. package/packages/convex/src/queries.ts +106 -0
  28. package/packages/convex/src/schema.ts +54 -0
  29. package/packages/convex/src/types.ts +20 -0
  30. package/packages/convex/tsconfig.json +11 -0
  31. package/packages/convex/tsup.config.ts +10 -0
  32. package/packages/react/README.md +1 -0
  33. package/packages/react/package.json +45 -0
  34. package/packages/react/src/components/AdapterCard.tsx +49 -0
  35. package/packages/react/src/components/AdapterConfigForm.tsx +118 -0
  36. package/packages/react/src/components/AdapterList.tsx +84 -0
  37. package/packages/react/src/components/AdapterStatusBadge.tsx +30 -0
  38. package/packages/react/src/components/index.ts +4 -0
  39. package/packages/react/src/hooks/index.ts +75 -0
  40. package/packages/react/src/index.tsx +44 -0
  41. package/packages/react/src/pages/AdapterDetailPage.tsx +133 -0
  42. package/packages/react/src/pages/AdaptersPage.tsx +111 -0
  43. package/packages/react/src/pages/index.ts +2 -0
  44. package/packages/react/src/provider/AdapterProvider.tsx +115 -0
  45. package/packages/react/src/provider/index.ts +2 -0
  46. package/packages/react/tsconfig.json +18 -0
  47. package/packages/react/tsup.config.ts +10 -0
  48. package/packages/react-css/README.md +1 -0
  49. package/packages/react-css/package.json +44 -0
  50. package/packages/react-css/src/adapters.css +1576 -0
  51. package/packages/react-css/src/components/AdapterCard.tsx +34 -0
  52. package/packages/react-css/src/components/AdapterConfigForm.tsx +63 -0
  53. package/packages/react-css/src/components/AdapterList.tsx +40 -0
  54. package/packages/react-css/src/components/AdapterStatusBadge.tsx +21 -0
  55. package/packages/react-css/src/components/index.ts +4 -0
  56. package/packages/react-css/src/hooks/index.ts +75 -0
  57. package/packages/react-css/src/index.tsx +25 -0
  58. package/packages/react-css/src/pages/AdapterDetailPage.tsx +133 -0
  59. package/packages/react-css/src/pages/AdaptersPage.tsx +111 -0
  60. package/packages/react-css/src/pages/index.ts +2 -0
  61. package/packages/react-css/src/provider/AdapterProvider.tsx +115 -0
  62. package/packages/react-css/src/provider/index.ts +2 -0
  63. package/packages/react-css/src/styles.css +494 -0
  64. package/packages/react-css/tsconfig.json +19 -0
  65. package/packages/react-css/tsup.config.ts +2 -0
  66. package/packages/shared/README.md +1 -0
  67. package/packages/shared/package.json +39 -0
  68. package/packages/shared/src/__tests__/adapters.test.ts +545 -0
  69. package/packages/shared/src/admin/index.ts +2 -0
  70. package/packages/shared/src/admin/interface.ts +34 -0
  71. package/packages/shared/src/admin/localStorage.ts +109 -0
  72. package/packages/shared/src/ai/anthropic.ts +123 -0
  73. package/packages/shared/src/ai/cloudflare-gateway.ts +130 -0
  74. package/packages/shared/src/ai/gemini.ts +181 -0
  75. package/packages/shared/src/ai/index.ts +14 -0
  76. package/packages/shared/src/ai/interface.ts +11 -0
  77. package/packages/shared/src/ai/localStorage.ts +78 -0
  78. package/packages/shared/src/ai/ollama.ts +143 -0
  79. package/packages/shared/src/ai/openai.ts +120 -0
  80. package/packages/shared/src/ai/vercel-ai.ts +101 -0
  81. package/packages/shared/src/auth/better-auth.ts +118 -0
  82. package/packages/shared/src/auth/clerk.ts +151 -0
  83. package/packages/shared/src/auth/convex-auth.ts +125 -0
  84. package/packages/shared/src/auth/index.ts +10 -0
  85. package/packages/shared/src/auth/interface.ts +17 -0
  86. package/packages/shared/src/auth/localStorage.ts +125 -0
  87. package/packages/shared/src/auth/supabase-auth.ts +136 -0
  88. package/packages/shared/src/config.ts +57 -0
  89. package/packages/shared/src/constants.ts +122 -0
  90. package/packages/shared/src/db/convex.ts +146 -0
  91. package/packages/shared/src/db/index.ts +10 -0
  92. package/packages/shared/src/db/interface.ts +13 -0
  93. package/packages/shared/src/db/localStorage.ts +91 -0
  94. package/packages/shared/src/db/mongodb.ts +125 -0
  95. package/packages/shared/src/db/neon.ts +171 -0
  96. package/packages/shared/src/db/supabase.ts +158 -0
  97. package/packages/shared/src/index.ts +117 -0
  98. package/packages/shared/src/payments/index.ts +4 -0
  99. package/packages/shared/src/payments/interface.ts +11 -0
  100. package/packages/shared/src/payments/localStorage.ts +81 -0
  101. package/packages/shared/src/payments/stripe.ts +177 -0
  102. package/packages/shared/src/storage/convex.ts +113 -0
  103. package/packages/shared/src/storage/index.ts +14 -0
  104. package/packages/shared/src/storage/interface.ts +11 -0
  105. package/packages/shared/src/storage/localStorage.ts +95 -0
  106. package/packages/shared/src/storage/minio.ts +47 -0
  107. package/packages/shared/src/storage/r2.ts +123 -0
  108. package/packages/shared/src/storage/s3.ts +128 -0
  109. package/packages/shared/src/storage/supabase-storage.ts +116 -0
  110. package/packages/shared/src/storage/uploadthing.ts +126 -0
  111. package/packages/shared/src/styles/adapters.css +494 -0
  112. package/packages/shared/src/tier-gate.ts +119 -0
  113. package/packages/shared/src/types.ts +162 -0
  114. package/packages/shared/tsconfig.json +18 -0
  115. package/packages/shared/tsup.config.ts +9 -0
  116. package/packages/shared/vitest.config.ts +14 -0
  117. package/packages/solidjs/README.md +1 -0
  118. package/packages/solidjs/package.json +44 -0
  119. package/packages/solidjs/src/components/AdapterCard.tsx +24 -0
  120. package/packages/solidjs/src/components/AdapterConfigForm.tsx +54 -0
  121. package/packages/solidjs/src/components/AdapterList.tsx +28 -0
  122. package/packages/solidjs/src/components/AdapterStatusBadge.tsx +20 -0
  123. package/packages/solidjs/src/components/index.ts +4 -0
  124. package/packages/solidjs/src/index.tsx +17 -0
  125. package/packages/solidjs/src/pages/AdapterDetailPage.tsx +38 -0
  126. package/packages/solidjs/src/pages/AdaptersPage.tsx +39 -0
  127. package/packages/solidjs/src/pages/index.ts +2 -0
  128. package/packages/solidjs/src/primitives/index.ts +78 -0
  129. package/packages/solidjs/src/provider/AdapterProvider.tsx +62 -0
  130. package/packages/solidjs/src/provider/index.ts +2 -0
  131. package/packages/solidjs/tsconfig.json +20 -0
  132. package/packages/solidjs/tsup.config.ts +10 -0
  133. package/packages/solidjs-css/README.md +1 -0
  134. package/packages/solidjs-css/package.json +43 -0
  135. package/packages/solidjs-css/src/adapters.css +1576 -0
  136. package/packages/solidjs-css/src/components/AdapterCard.tsx +43 -0
  137. package/packages/solidjs-css/src/components/AdapterConfigForm.tsx +119 -0
  138. package/packages/solidjs-css/src/components/AdapterList.tsx +68 -0
  139. package/packages/solidjs-css/src/components/AdapterStatusBadge.tsx +24 -0
  140. package/packages/solidjs-css/src/components/index.ts +8 -0
  141. package/packages/solidjs-css/src/index.tsx +30 -0
  142. package/packages/solidjs-css/src/pages/AdapterDetailPage.tsx +107 -0
  143. package/packages/solidjs-css/src/pages/AdaptersPage.tsx +94 -0
  144. package/packages/solidjs-css/src/pages/index.ts +4 -0
  145. package/packages/solidjs-css/src/primitives/index.ts +1 -0
  146. package/packages/solidjs-css/src/provider/AdapterProvider.tsx +61 -0
  147. package/packages/solidjs-css/src/provider/index.ts +2 -0
  148. package/packages/solidjs-css/tsconfig.json +20 -0
  149. package/packages/solidjs-css/tsup.config.ts +2 -0
  150. package/pnpm-workspace.yaml +2 -0
  151. package/tsconfig.json +17 -0
@@ -0,0 +1,34 @@
1
+ // @geenius-adapters/react-css — AdapterCard
2
+
3
+ import type { AdapterDomain, AdapterStatusInfo } from '@geenius-adapters/shared'
4
+ import { DOMAIN_LABELS, DOMAIN_ICONS, DOMAIN_DESCRIPTIONS } from '@geenius-adapters/shared'
5
+ import { AdapterStatusBadge } from './AdapterStatusBadge'
6
+
7
+ export interface AdapterCardProps {
8
+ domain: AdapterDomain
9
+ status: AdapterStatusInfo
10
+ onClick?: () => void
11
+ className?: string
12
+ }
13
+
14
+ export function AdapterCard({ domain, status, onClick, className = '' }: AdapterCardProps) {
15
+ return (
16
+ <button type="button" onClick={onClick} className={`adapter-card ${className}`} aria-label={`${DOMAIN_LABELS[domain]} adapter`}>
17
+ <div className="adapter-card__icon">{DOMAIN_ICONS[domain]}</div>
18
+ <div className="adapter-card__body">
19
+ <div className="adapter-card__header">
20
+ <h3 className="adapter-card__name">{DOMAIN_LABELS[domain]}</h3>
21
+ <AdapterStatusBadge status={status.status} />
22
+ </div>
23
+ <p className="adapter-card__desc">{DOMAIN_DESCRIPTIONS[domain]}</p>
24
+ {status.provider && status.provider !== 'none' && (
25
+ <div className="adapter-card__meta">
26
+ <span className="adapter-card__provider">{status.provider}</span>
27
+ {status.latency && <span className="adapter-card__latency">{status.latency}ms</span>}
28
+ </div>
29
+ )}
30
+ {status.error && <p className="adapter-card__error">{status.error}</p>}
31
+ </div>
32
+ </button>
33
+ )
34
+ }
@@ -0,0 +1,63 @@
1
+ // @geenius-adapters/react-css — AdapterConfigForm
2
+
3
+ import { useState } from 'react'
4
+ import type { AdapterDomain, DomainAdapterConfig } from '@geenius-adapters/shared'
5
+ import { DOMAIN_LABELS, getProvidersForDomain } from '@geenius-adapters/shared'
6
+
7
+ export interface AdapterConfigFormProps {
8
+ domain: AdapterDomain
9
+ initialConfig?: DomainAdapterConfig
10
+ onSave: (config: DomainAdapterConfig) => Promise<void> | void
11
+ onCancel?: () => void
12
+ className?: string
13
+ }
14
+
15
+ export function AdapterConfigForm({ domain, initialConfig, onSave, onCancel, className = '' }: AdapterConfigFormProps) {
16
+ const providers = getProvidersForDomain(domain)
17
+ const [provider, setProvider] = useState(initialConfig?.provider ?? providers[0]?.id ?? '')
18
+ const [apiKey, setApiKey] = useState(initialConfig?.apiKey ?? '')
19
+ const [baseUrl, setBaseUrl] = useState(initialConfig?.baseUrl ?? '')
20
+ const [isSaving, setIsSaving] = useState(false)
21
+ const [error, setError] = useState<string | null>(null)
22
+ const needsApiKey = !['localStorage', 'noop'].includes(provider)
23
+ const needsBaseUrl = ['ollama', 'minio', 'neon', 'supabase'].includes(provider)
24
+
25
+ const handleSubmit = async (e: React.FormEvent) => {
26
+ e.preventDefault(); setError(null)
27
+ if (!provider) { setError('Select a provider'); return }
28
+ setIsSaving(true)
29
+ try { await onSave({ provider, apiKey: apiKey || undefined, baseUrl: baseUrl || undefined }) }
30
+ catch (e) { setError(e instanceof Error ? e.message : 'Failed') }
31
+ finally { setIsSaving(false) }
32
+ }
33
+
34
+ return (
35
+ <form onSubmit={handleSubmit} className={`adapter-form ${className}`} noValidate>
36
+ <h3 className="adapter-form__title">Configure {DOMAIN_LABELS[domain]}</h3>
37
+ <p className="adapter-form__subtitle">Select your preferred provider.</p>
38
+ {error && <div className="adapter-alert adapter-alert--error" role="alert" style={{marginTop: '1rem'}}>{error}</div>}
39
+ <div className="adapter-field">
40
+ <span className="adapter-field__label">Provider</span>
41
+ <div className="adapter-provider-grid">
42
+ {providers.map(p => (
43
+ <button key={p.id} type="button" onClick={() => setProvider(p.id)} className={`adapter-provider-option ${provider === p.id ? 'adapter-provider-option--selected' : ''}`}>
44
+ <span className="adapter-provider-option__name">{p.name}</span>
45
+ <span className="adapter-provider-option__desc">{p.description}</span>
46
+ <span className={`adapter-tier-badge adapter-tier-badge--${p.tier}`}>{p.tier}</span>
47
+ </button>
48
+ ))}
49
+ </div>
50
+ </div>
51
+ {needsApiKey && (
52
+ <div className="adapter-field"><label className="adapter-field__label">API Key</label><input type="password" value={apiKey} onChange={e => setApiKey(e.target.value)} className="adapter-field__input" placeholder="Enter API key" /></div>
53
+ )}
54
+ {needsBaseUrl && (
55
+ <div className="adapter-field"><label className="adapter-field__label">Base URL</label><input type="url" value={baseUrl} onChange={e => setBaseUrl(e.target.value)} className="adapter-field__input" placeholder="https://..." /></div>
56
+ )}
57
+ <div style={{display: 'flex', justifyContent: 'flex-end', gap: '0.75rem', marginTop: '1.5rem'}}>
58
+ {onCancel && <button type="button" onClick={onCancel} className="adapter-btn adapter-btn--ghost">Cancel</button>}
59
+ <button type="submit" disabled={isSaving || !provider} className="adapter-btn adapter-btn--primary">{isSaving ? 'Saving…' : 'Save'}</button>
60
+ </div>
61
+ </form>
62
+ )
63
+ }
@@ -0,0 +1,40 @@
1
+ // @geenius-adapters/react-css — AdapterList
2
+
3
+ import { useState, useMemo } from 'react'
4
+ import { useAdapters } from '../hooks'
5
+ import { AdapterCard } from './AdapterCard'
6
+ import { ADAPTER_DOMAINS, DOMAIN_LABELS } from '@geenius-adapters/shared'
7
+ import type { AdapterDomain, AdapterStatusType } from '@geenius-adapters/shared'
8
+
9
+ export interface AdapterListProps {
10
+ onSelect?: (domain: AdapterDomain) => void
11
+ filterStatus?: AdapterStatusType
12
+ className?: string
13
+ }
14
+
15
+ export function AdapterList({ onSelect, filterStatus, className = '' }: AdapterListProps) {
16
+ const { statuses, isLoading } = useAdapters()
17
+ const [search, setSearch] = useState('')
18
+
19
+ const filtered = useMemo(() => {
20
+ let domains = [...ADAPTER_DOMAINS]
21
+ if (search) { const q = search.toLowerCase(); domains = domains.filter(d => DOMAIN_LABELS[d].toLowerCase().includes(q) || d.includes(q)) }
22
+ if (filterStatus) { domains = domains.filter(d => statuses[d]?.status === filterStatus) }
23
+ return domains
24
+ }, [search, filterStatus, statuses])
25
+
26
+ if (isLoading) return (
27
+ <div className={className} role="status">{[1,2,3,4].map(i => <div key={i} className="adapter-skeleton" style={{height: '6rem', marginBottom: '0.75rem'}} />)}</div>
28
+ )
29
+
30
+ return (
31
+ <div className={className}>
32
+ <input type="text" value={search} onChange={e => setSearch(e.target.value)} placeholder="Search adapters..." className="adapter-search" aria-label="Search adapters" />
33
+ {filtered.length === 0 ? (
34
+ <div className="adapter-empty"><p className="adapter-empty__text">{search ? 'No match' : 'No adapters configured'}</p></div>
35
+ ) : (
36
+ <div className="adapter-grid" role="list">{filtered.map(d => <AdapterCard key={d} domain={d} status={statuses[d]} onClick={() => onSelect?.(d)} />)}</div>
37
+ )}
38
+ </div>
39
+ )
40
+ }
@@ -0,0 +1,21 @@
1
+ // @geenius-adapters/react-css — AdapterStatusBadge
2
+
3
+ import type { AdapterStatusType } from '@geenius-adapters/shared'
4
+
5
+ export interface AdapterStatusBadgeProps {
6
+ status: AdapterStatusType
7
+ showLabel?: boolean
8
+ className?: string
9
+ }
10
+
11
+ export function AdapterStatusBadge({ status, showLabel = true, className = '' }: AdapterStatusBadgeProps) {
12
+ const labels: Record<AdapterStatusType, string> = {
13
+ connected: 'Connected', disconnected: 'Disconnected', error: 'Error', initializing: 'Initializing',
14
+ }
15
+ return (
16
+ <span className={`adapter-badge adapter-badge--${status} ${className}`}>
17
+ <span className="adapter-badge__dot" />
18
+ {showLabel && <span>{labels[status]}</span>}
19
+ </span>
20
+ )
21
+ }
@@ -0,0 +1,4 @@
1
+ export { AdapterStatusBadge, type AdapterStatusBadgeProps } from './AdapterStatusBadge'
2
+ export { AdapterCard, type AdapterCardProps } from './AdapterCard'
3
+ export { AdapterList, type AdapterListProps } from './AdapterList'
4
+ export { AdapterConfigForm, type AdapterConfigFormProps } from './AdapterConfigForm'
@@ -0,0 +1,75 @@
1
+ // @geenius-adapters/react — Hooks
2
+
3
+ import { useMemo } from 'react'
4
+ import { useAdapterContext } from '../provider/AdapterProvider'
5
+ import type {
6
+ AuthAdapter, DbAdapter, PaymentsAdapter, AiAdapter, FileStorageAdapter, AdminAdapter,
7
+ AdapterDomain, AdapterStatusInfo,
8
+ } from '@geenius-adapters/shared'
9
+
10
+ // ─── Domain-specific hooks ───────────────────────────────────────────────────
11
+
12
+ /** Access the database adapter */
13
+ export function useDb(): DbAdapter {
14
+ return useAdapterContext().getAdapter('db')
15
+ }
16
+
17
+ /** Access the auth adapter */
18
+ export function useAuth(): AuthAdapter {
19
+ return useAdapterContext().getAdapter('auth')
20
+ }
21
+
22
+ /** Access the payments adapter */
23
+ export function usePayments(): PaymentsAdapter {
24
+ return useAdapterContext().getAdapter('payments')
25
+ }
26
+
27
+ /** Access the AI adapter */
28
+ export function useAi(): AiAdapter {
29
+ return useAdapterContext().getAdapter('ai')
30
+ }
31
+
32
+ /** Access the file storage adapter */
33
+ export function useStorage(): FileStorageAdapter {
34
+ return useAdapterContext().getAdapter('storage')
35
+ }
36
+
37
+ /** Access the admin adapter */
38
+ export function useAdmin(): AdminAdapter {
39
+ return useAdapterContext().getAdapter('admin')
40
+ }
41
+
42
+ // ─── Status hooks ────────────────────────────────────────────────────────────
43
+
44
+ /** Get all adapter statuses */
45
+ export function useAdapterStatuses(): Record<AdapterDomain, AdapterStatusInfo> {
46
+ return useAdapterContext().statuses
47
+ }
48
+
49
+ /** Get status for a specific adapter domain */
50
+ export function useAdapterStatus(domain: AdapterDomain): AdapterStatusInfo {
51
+ return useAdapterContext().statuses[domain]
52
+ }
53
+
54
+ /** Check if adapter is ready */
55
+ export function useIsAdapterReady(domain: AdapterDomain): boolean {
56
+ return useAdapterContext().isReady(domain)
57
+ }
58
+
59
+ // ─── Full adapters hook ──────────────────────────────────────────────────────
60
+
61
+ /** Access all adapters at once */
62
+ export function useAdapters() {
63
+ const ctx = useAdapterContext()
64
+ return useMemo(() => ({
65
+ db: ctx.adapters.db,
66
+ auth: ctx.adapters.auth,
67
+ payments: ctx.adapters.payments,
68
+ ai: ctx.adapters.ai,
69
+ storage: ctx.adapters.storage,
70
+ admin: ctx.adapters.admin,
71
+ statuses: ctx.statuses,
72
+ isLoading: ctx.isLoading,
73
+ isReady: ctx.isReady,
74
+ }), [ctx])
75
+ }
@@ -0,0 +1,25 @@
1
+ // @geenius-adapters/react-css — Barrel export
2
+
3
+ /**
4
+ * @geenius-adapters/react-css
5
+ * React components using vanilla CSS (no Tailwind).
6
+ * Import: `import '@geenius-adapters/react-css/styles.css'`
7
+ */
8
+
9
+ export type {
10
+ AuthAdapter, DbAdapter, PaymentsAdapter, AiAdapter, FileStorageAdapter,
11
+ AdminAdapter, AdapterDomain, AdapterConfig, AdapterStatusType, AdapterStatusInfo,
12
+ } from '@geenius-adapters/shared'
13
+
14
+ export { ADAPTER_DOMAINS, DOMAIN_LABELS, DOMAIN_ICONS, DOMAIN_DESCRIPTIONS, configureAdapters, getAdapterConfig } from '@geenius-adapters/shared'
15
+
16
+ export { AdapterProvider, useAdapterContext } from './provider'
17
+ export type { AdapterProviderProps, AdapterSet, AdapterContextValue } from './provider'
18
+
19
+ export { useDb, useAuth, usePayments, useAi, useStorage, useAdmin, useAdapters, useAdapterStatuses, useAdapterStatus, useIsAdapterReady } from './hooks'
20
+
21
+ export { AdapterStatusBadge, AdapterCard, AdapterList, AdapterConfigForm } from './components'
22
+ export type { AdapterStatusBadgeProps, AdapterCardProps, AdapterListProps, AdapterConfigFormProps } from './components'
23
+
24
+ export { AdaptersPage, AdapterDetailPage } from './pages'
25
+ export type { AdaptersPageProps, AdapterDetailPageProps } from './pages'
@@ -0,0 +1,133 @@
1
+ // @geenius-adapters/react — AdapterDetailPage
2
+
3
+ import { useState } from 'react'
4
+ import { useAdapters, useAdapterStatus } from '../hooks'
5
+ import { AdapterStatusBadge } from '../components/AdapterStatusBadge'
6
+ import { AdapterConfigForm } from '../components/AdapterConfigForm'
7
+ import type { AdapterDomain } from '@geenius-adapters/shared'
8
+ import { DOMAIN_LABELS, DOMAIN_ICONS, DOMAIN_DESCRIPTIONS, getProvidersForDomain } from '@geenius-adapters/shared'
9
+
10
+ export interface AdapterDetailPageProps {
11
+ domain: AdapterDomain
12
+ onBack?: () => void
13
+ className?: string
14
+ }
15
+
16
+ export function AdapterDetailPage({ domain, onBack, className = '' }: AdapterDetailPageProps) {
17
+ const { isLoading } = useAdapters()
18
+ const status = useAdapterStatus(domain)
19
+ const [isEditing, setIsEditing] = useState(false)
20
+ const providers = getProvidersForDomain(domain)
21
+
22
+ const label = DOMAIN_LABELS[domain]
23
+ const icon = DOMAIN_ICONS[domain]
24
+ const desc = DOMAIN_DESCRIPTIONS[domain]
25
+
26
+ if (isLoading) {
27
+ return (
28
+ <div className={`max-w-3xl mx-auto space-y-6 ${className}`}>
29
+ <div className="animate-pulse space-y-4">
30
+ <div className="h-6 w-32 rounded bg-white/10" />
31
+ <div className="h-40 rounded-2xl bg-white/[0.03]" />
32
+ <div className="h-60 rounded-2xl bg-white/[0.03]" />
33
+ </div>
34
+ </div>
35
+ )
36
+ }
37
+
38
+ return (
39
+ <div className={`max-w-3xl mx-auto space-y-8 ${className}`}>
40
+ {/* Back navigation */}
41
+ {onBack && (
42
+ <button onClick={onBack} className="flex items-center gap-1.5 text-sm text-white/30 hover:text-white/60 transition-colors">
43
+ <span>←</span> Back to Adapters
44
+ </button>
45
+ )}
46
+
47
+ {/* Header */}
48
+ <div className="flex items-start gap-5">
49
+ <div className="flex h-16 w-16 items-center justify-center rounded-2xl bg-white/5 text-3xl">{icon}</div>
50
+ <div className="flex-1">
51
+ <div className="flex items-center gap-3">
52
+ <h1 className="text-2xl font-extrabold text-white/95 tracking-tight">{label}</h1>
53
+ <AdapterStatusBadge status={status.status} size="md" />
54
+ </div>
55
+ <p className="mt-1 text-sm text-white/40">{desc}</p>
56
+ </div>
57
+ </div>
58
+
59
+ {/* Status Card */}
60
+ <div className="rounded-2xl border border-white/10 bg-white/[0.03] p-6 backdrop-blur-sm">
61
+ <h2 className="text-xs font-semibold uppercase tracking-wider text-white/30 mb-4">Current Status</h2>
62
+ <div className="grid gap-4 sm:grid-cols-3">
63
+ <div>
64
+ <p className="text-[0.625rem] text-white/30 uppercase">Provider</p>
65
+ <p className="mt-0.5 text-sm font-semibold text-white/80">{status.provider !== 'none' ? status.provider : '—'}</p>
66
+ </div>
67
+ <div>
68
+ <p className="text-[0.625rem] text-white/30 uppercase">Status</p>
69
+ <p className="mt-0.5"><AdapterStatusBadge status={status.status} /></p>
70
+ </div>
71
+ <div>
72
+ <p className="text-[0.625rem] text-white/30 uppercase">Last Checked</p>
73
+ <p className="mt-0.5 text-sm text-white/60">{status.lastCheckedAt ? new Date(status.lastCheckedAt).toLocaleTimeString() : '—'}</p>
74
+ </div>
75
+ </div>
76
+ {status.error && (
77
+ <div className="mt-4 rounded-lg border border-red-500/20 bg-red-500/10 px-4 py-3 text-sm text-red-400">
78
+ <span className="font-semibold">Error:</span> {status.error}
79
+ </div>
80
+ )}
81
+ {status.latency && (
82
+ <div className="mt-4">
83
+ <p className="text-[0.625rem] text-white/30 uppercase mb-1">Latency</p>
84
+ <div className="h-2 w-full rounded-full bg-white/5 overflow-hidden">
85
+ <div className={`h-full rounded-full transition-all ${status.latency < 100 ? 'bg-emerald-400' : status.latency < 500 ? 'bg-amber-400' : 'bg-red-400'}`}
86
+ style={{ width: `${Math.min(100, (status.latency / 1000) * 100)}%` }} />
87
+ </div>
88
+ <p className="mt-1 text-xs text-white/40">{status.latency}ms</p>
89
+ </div>
90
+ )}
91
+ </div>
92
+
93
+ {/* Available Providers */}
94
+ <div className="rounded-2xl border border-white/10 bg-white/[0.03] p-6 backdrop-blur-sm">
95
+ <div className="flex items-center justify-between mb-4">
96
+ <h2 className="text-xs font-semibold uppercase tracking-wider text-white/30">Available Providers</h2>
97
+ <span className="text-xs text-white/20">{providers.length} providers</span>
98
+ </div>
99
+ <div className="divide-y divide-white/5">
100
+ {providers.map(p => (
101
+ <div key={p.id} className="flex items-center justify-between py-3 first:pt-0 last:pb-0">
102
+ <div>
103
+ <p className="text-sm font-semibold text-white/80">{p.name}</p>
104
+ <p className="text-xs text-white/30">{p.description}</p>
105
+ </div>
106
+ <span className={`self-start rounded px-2 py-0.5 text-[0.625rem] font-bold uppercase ${
107
+ p.tier === 'pronto' ? 'bg-emerald-500/10 text-emerald-400' : p.tier === 'mvp' ? 'bg-blue-500/10 text-blue-400' : 'bg-purple-500/10 text-purple-400'
108
+ }`}>{p.tier}</span>
109
+ </div>
110
+ ))}
111
+ </div>
112
+ </div>
113
+
114
+ {/* Configuration */}
115
+ <div>
116
+ {isEditing ? (
117
+ <AdapterConfigForm
118
+ domain={domain}
119
+ onSave={async () => setIsEditing(false)}
120
+ onCancel={() => setIsEditing(false)}
121
+ />
122
+ ) : (
123
+ <button
124
+ onClick={() => setIsEditing(true)}
125
+ className="w-full rounded-2xl border border-dashed border-white/10 bg-white/[0.01] p-6 text-center text-sm text-white/30 hover:border-white/20 hover:text-white/50 transition-all"
126
+ >
127
+ ⚙️ Configure {label} Adapter
128
+ </button>
129
+ )}
130
+ </div>
131
+ </div>
132
+ )
133
+ }
@@ -0,0 +1,111 @@
1
+ // @geenius-adapters/react — AdaptersPage
2
+
3
+ import { useState } from 'react'
4
+ import { useAdapters } from '../hooks'
5
+ import { AdapterList } from '../components/AdapterList'
6
+ import { AdapterConfigForm } from '../components/AdapterConfigForm'
7
+ import { AdapterStatusBadge } from '../components/AdapterStatusBadge'
8
+ import { ADAPTER_DOMAINS, ADAPTER_STATUSES, DOMAIN_LABELS } from '@geenius-adapters/shared'
9
+ import type { AdapterDomain, AdapterStatusType } from '@geenius-adapters/shared'
10
+
11
+ export interface AdaptersPageProps {
12
+ onNavigateDetail?: (domain: AdapterDomain) => void
13
+ className?: string
14
+ }
15
+
16
+ export function AdaptersPage({ onNavigateDetail, className = '' }: AdaptersPageProps) {
17
+ const { statuses, isLoading } = useAdapters()
18
+ const [selectedDomain, setSelectedDomain] = useState<AdapterDomain | null>(null)
19
+ const [filterStatus, setFilterStatus] = useState<AdapterStatusType | undefined>()
20
+
21
+ // Status summary counts
22
+ const counts = {
23
+ connected: ADAPTER_DOMAINS.filter(d => statuses[d]?.status === 'connected').length,
24
+ error: ADAPTER_DOMAINS.filter(d => statuses[d]?.status === 'error').length,
25
+ total: ADAPTER_DOMAINS.length,
26
+ }
27
+
28
+ if (isLoading) {
29
+ return (
30
+ <div className={`max-w-4xl mx-auto space-y-6 ${className}`}>
31
+ <div className="animate-pulse space-y-4">
32
+ <div className="h-8 w-48 rounded bg-white/10" />
33
+ <div className="h-4 w-72 rounded bg-white/5" />
34
+ <div className="grid gap-3 sm:grid-cols-3">{[1,2,3].map(i => <div key={i} className="h-20 rounded-2xl bg-white/[0.03]" />)}</div>
35
+ <div className="grid gap-3 sm:grid-cols-2">{[1,2,3,4].map(i => <div key={i} className="h-28 rounded-2xl bg-white/[0.03]" />)}</div>
36
+ </div>
37
+ </div>
38
+ )
39
+ }
40
+
41
+ return (
42
+ <div className={`max-w-4xl mx-auto space-y-8 ${className}`}>
43
+ {/* Header */}
44
+ <div>
45
+ <h1 className="text-2xl font-extrabold text-white/95 tracking-tight">Adapters</h1>
46
+ <p className="mt-1 text-sm text-white/40">Manage your infrastructure adapters — database, auth, AI, storage, payments, and admin.</p>
47
+ </div>
48
+
49
+ {/* Status Summary */}
50
+ <div className="grid gap-3 sm:grid-cols-3">
51
+ <div className="rounded-2xl border border-white/10 bg-white/[0.03] p-4 backdrop-blur-sm">
52
+ <p className="text-xs font-semibold uppercase tracking-wider text-white/30">Total</p>
53
+ <p className="mt-1 text-2xl font-extrabold text-white/90">{counts.total}</p>
54
+ <p className="text-[0.625rem] text-white/30">adapter domains</p>
55
+ </div>
56
+ <div className="rounded-2xl border border-emerald-500/10 bg-emerald-500/[0.03] p-4 backdrop-blur-sm">
57
+ <p className="text-xs font-semibold uppercase tracking-wider text-emerald-400/60">Connected</p>
58
+ <p className="mt-1 text-2xl font-extrabold text-emerald-400">{counts.connected}</p>
59
+ <p className="text-[0.625rem] text-emerald-400/40">active adapters</p>
60
+ </div>
61
+ {counts.error > 0 && (
62
+ <div className="rounded-2xl border border-red-500/10 bg-red-500/[0.03] p-4 backdrop-blur-sm">
63
+ <p className="text-xs font-semibold uppercase tracking-wider text-red-400/60">Errors</p>
64
+ <p className="mt-1 text-2xl font-extrabold text-red-400">{counts.error}</p>
65
+ <p className="text-[0.625rem] text-red-400/40">need attention</p>
66
+ </div>
67
+ )}
68
+ </div>
69
+
70
+ {/* Filters */}
71
+ <div className="flex items-center gap-2">
72
+ <button
73
+ onClick={() => setFilterStatus(undefined)}
74
+ className={`rounded-lg px-3 py-1.5 text-xs font-semibold transition-all ${!filterStatus ? 'bg-white/10 text-white/80' : 'text-white/30 hover:text-white/50'}`}
75
+ >All</button>
76
+ {ADAPTER_STATUSES.map(s => (
77
+ <button
78
+ key={s}
79
+ onClick={() => setFilterStatus(s)}
80
+ className={`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-semibold transition-all ${filterStatus === s ? 'bg-white/10 text-white/80' : 'text-white/30 hover:text-white/50'}`}
81
+ >
82
+ <AdapterStatusBadge status={s} showLabel={false} size="sm" />
83
+ {s}
84
+ </button>
85
+ ))}
86
+ </div>
87
+
88
+ {/* Adapter Grid */}
89
+ <AdapterList
90
+ onSelect={(domain) => {
91
+ if (onNavigateDetail) { onNavigateDetail(domain) }
92
+ else { setSelectedDomain(domain) }
93
+ }}
94
+ filterStatus={filterStatus}
95
+ />
96
+
97
+ {/* Inline Config Modal */}
98
+ {selectedDomain && (
99
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={() => setSelectedDomain(null)}>
100
+ <div className="max-w-lg w-full mx-4" onClick={e => e.stopPropagation()}>
101
+ <AdapterConfigForm
102
+ domain={selectedDomain}
103
+ onSave={async () => { setSelectedDomain(null) }}
104
+ onCancel={() => setSelectedDomain(null)}
105
+ />
106
+ </div>
107
+ </div>
108
+ )}
109
+ </div>
110
+ )
111
+ }
@@ -0,0 +1,2 @@
1
+ export { AdaptersPage, type AdaptersPageProps } from './AdaptersPage'
2
+ export { AdapterDetailPage, type AdapterDetailPageProps } from './AdapterDetailPage'
@@ -0,0 +1,115 @@
1
+ // @geenius-adapters/react — Provider
2
+
3
+ 'use client'
4
+
5
+ import { createContext, useContext, useState, useCallback, useEffect, useMemo, type ReactNode } from 'react'
6
+ import type {
7
+ AuthAdapter, DbAdapter, PaymentsAdapter, AiAdapter, FileStorageAdapter, AdminAdapter,
8
+ AdapterDomain, AdapterStatusType, AdapterStatusInfo,
9
+ } from '@geenius-adapters/shared'
10
+ import { ADAPTER_DOMAINS, DOMAIN_LABELS } from '@geenius-adapters/shared'
11
+
12
+ // ─── Adapter Set ─────────────────────────────────────────────────────────────
13
+
14
+ export interface AdapterSet {
15
+ db?: DbAdapter
16
+ auth?: AuthAdapter
17
+ payments?: PaymentsAdapter
18
+ ai?: AiAdapter
19
+ storage?: FileStorageAdapter
20
+ admin?: AdminAdapter
21
+ }
22
+
23
+ // ─── Context ─────────────────────────────────────────────────────────────────
24
+
25
+ export interface AdapterContextValue {
26
+ adapters: AdapterSet
27
+ statuses: Record<AdapterDomain, AdapterStatusInfo>
28
+ getAdapter: <K extends keyof AdapterSet>(domain: K) => NonNullable<AdapterSet[K]>
29
+ isReady: (domain: AdapterDomain) => boolean
30
+ isLoading: boolean
31
+ }
32
+
33
+ const AdapterContext = createContext<AdapterContextValue | null>(null)
34
+
35
+ // ─── Provider ────────────────────────────────────────────────────────────────
36
+
37
+ export interface AdapterProviderProps {
38
+ adapters: AdapterSet
39
+ children: ReactNode
40
+ /** Check adapter health on mount */
41
+ healthCheck?: boolean
42
+ }
43
+
44
+ export function AdapterProvider({ adapters, children, healthCheck = false }: AdapterProviderProps) {
45
+ const [isLoading, setIsLoading] = useState(healthCheck)
46
+ const [statuses, setStatuses] = useState<Record<AdapterDomain, AdapterStatusInfo>>(() => {
47
+ const initial = {} as Record<AdapterDomain, AdapterStatusInfo>
48
+ for (const domain of ADAPTER_DOMAINS) {
49
+ const adapter = adapters[domain as keyof AdapterSet]
50
+ initial[domain] = {
51
+ domain,
52
+ provider: adapter ? 'configured' : 'none',
53
+ status: adapter ? 'connected' : 'disconnected',
54
+ lastCheckedAt: Date.now(),
55
+ }
56
+ }
57
+ return initial
58
+ })
59
+
60
+ useEffect(() => {
61
+ if (!healthCheck) return
62
+ setIsLoading(true)
63
+ // Simple health check — verify adapters are callable
64
+ const checks = ADAPTER_DOMAINS.map(async (domain) => {
65
+ const adapter = adapters[domain as keyof AdapterSet]
66
+ if (!adapter) return
67
+ try {
68
+ setStatuses(prev => ({ ...prev, [domain]: { ...prev[domain], status: 'initializing' } }))
69
+ // Basic connectivity test for each domain
70
+ if (domain === 'auth' && (adapter as AuthAdapter).getSession) {
71
+ await (adapter as AuthAdapter).getSession()
72
+ }
73
+ setStatuses(prev => ({
74
+ ...prev,
75
+ [domain]: { ...prev[domain], status: 'connected' as AdapterStatusType, lastCheckedAt: Date.now() },
76
+ }))
77
+ } catch (e) {
78
+ setStatuses(prev => ({
79
+ ...prev,
80
+ [domain]: {
81
+ ...prev[domain],
82
+ status: 'error' as AdapterStatusType,
83
+ error: e instanceof Error ? e.message : 'Health check failed',
84
+ lastCheckedAt: Date.now(),
85
+ },
86
+ }))
87
+ }
88
+ })
89
+ Promise.allSettled(checks).finally(() => setIsLoading(false))
90
+ }, [adapters, healthCheck])
91
+
92
+ const getAdapter = useCallback(<K extends keyof AdapterSet>(domain: K): NonNullable<AdapterSet[K]> => {
93
+ const adapter = adapters[domain]
94
+ if (!adapter) throw new Error(`${DOMAIN_LABELS[domain as AdapterDomain] ?? domain} adapter not configured in <AdapterProvider>`)
95
+ return adapter as NonNullable<AdapterSet[K]>
96
+ }, [adapters])
97
+
98
+ const isReady = useCallback((domain: AdapterDomain) => {
99
+ return !!adapters[domain as keyof AdapterSet] && statuses[domain]?.status === 'connected'
100
+ }, [adapters, statuses])
101
+
102
+ const value = useMemo<AdapterContextValue>(() => ({
103
+ adapters, statuses, getAdapter, isReady, isLoading,
104
+ }), [adapters, statuses, getAdapter, isReady, isLoading])
105
+
106
+ return <AdapterContext.Provider value={value}>{children}</AdapterContext.Provider>
107
+ }
108
+
109
+ // ─── Context Hook ────────────────────────────────────────────────────────────
110
+
111
+ export function useAdapterContext(): AdapterContextValue {
112
+ const ctx = useContext(AdapterContext)
113
+ if (!ctx) throw new Error('useAdapterContext must be used within <AdapterProvider>')
114
+ return ctx
115
+ }
@@ -0,0 +1,2 @@
1
+ export { AdapterProvider, useAdapterContext } from './AdapterProvider'
2
+ export type { AdapterProviderProps, AdapterSet, AdapterContextValue } from './AdapterProvider'