@burdenoff/microfe-subscriberbot 2026.1030.7 → 2026.1102.1
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.
- package/README.md +26 -0
- package/dist/stories/pages/AgentDetailPage.stories.d.ts +16 -0
- package/dist/stories/pages/AgentDetailPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/AgentNewPage.stories.d.ts +12 -0
- package/dist/stories/pages/AgentNewPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/AgentsListPage.stories.d.ts +16 -0
- package/dist/stories/pages/AgentsListPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/AssetVaultPage.stories.d.ts +14 -0
- package/dist/stories/pages/AssetVaultPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/DashboardPage.stories.d.ts +12 -0
- package/dist/stories/pages/DashboardPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/DecisionDetailPage.stories.d.ts +13 -0
- package/dist/stories/pages/DecisionDetailPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/DecisionsPage.stories.d.ts +14 -0
- package/dist/stories/pages/DecisionsPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/EnterpriseSpendPage.stories.d.ts +13 -0
- package/dist/stories/pages/EnterpriseSpendPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/InboxPage.stories.d.ts +20 -0
- package/dist/stories/pages/InboxPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/InsightsPage.stories.d.ts +12 -0
- package/dist/stories/pages/InsightsPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/InvoicesPage.stories.d.ts +16 -0
- package/dist/stories/pages/InvoicesPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/NegotiationDetailPage.stories.d.ts +11 -0
- package/dist/stories/pages/NegotiationDetailPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/NegotiationNewPage.stories.d.ts +9 -0
- package/dist/stories/pages/NegotiationNewPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/NegotiationsPage.stories.d.ts +11 -0
- package/dist/stories/pages/NegotiationsPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/ProviderDetailPage.stories.d.ts +15 -0
- package/dist/stories/pages/ProviderDetailPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/ProviderNewPage.stories.d.ts +12 -0
- package/dist/stories/pages/ProviderNewPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/ProvidersListPage.stories.d.ts +17 -0
- package/dist/stories/pages/ProvidersListPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/RelationshipDetailPage.stories.d.ts +15 -0
- package/dist/stories/pages/RelationshipDetailPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/RelationshipNewPage.stories.d.ts +12 -0
- package/dist/stories/pages/RelationshipNewPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/RelationshipsListPage.stories.d.ts +14 -0
- package/dist/stories/pages/RelationshipsListPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/SavingsPage.stories.d.ts +13 -0
- package/dist/stories/pages/SavingsPage.stories.d.ts.map +1 -0
- package/dist/stories/pages/WorkflowsPage.stories.d.ts +16 -0
- package/dist/stories/pages/WorkflowsPage.stories.d.ts.map +1 -0
- package/dist/stories/support/PageHarness.d.ts +28 -0
- package/dist/stories/support/PageHarness.d.ts.map +1 -0
- package/dist/stories/support/fixtures.d.ts +28 -0
- package/dist/stories/support/fixtures.d.ts.map +1 -0
- package/dist/stories/support/graphqlMock.d.ts +34 -0
- package/dist/stories/support/graphqlMock.d.ts.map +1 -0
- package/dist/stories/support/handlers.d.ts +8 -0
- package/dist/stories/support/handlers.d.ts.map +1 -0
- package/dist/stories/support/negotiationFixtures.d.ts +14 -0
- package/dist/stories/support/negotiationFixtures.d.ts.map +1 -0
- package/dist/stories/support/play.d.ts +7 -0
- package/dist/stories/support/play.d.ts.map +1 -0
- package/dist/subscriberbot/SubscriberBotRoutes.d.ts.map +1 -1
- package/dist/subscriberbot/SubscriberBotRoutes.js +18 -6
- package/dist/subscriberbot/SubscriberBotRoutes.js.map +1 -1
- package/dist/subscriberbot/admin/AdminDashboardPage.d.ts.map +1 -1
- package/dist/subscriberbot/admin/AdminDashboardPage.js +61 -51
- package/dist/subscriberbot/admin/AdminDashboardPage.js.map +1 -1
- package/dist/subscriberbot/admin/NegotiationDeskPanel.d.ts +4 -0
- package/dist/subscriberbot/admin/NegotiationDeskPanel.d.ts.map +1 -0
- package/dist/subscriberbot/admin/NegotiationDeskPanel.js +210 -0
- package/dist/subscriberbot/admin/NegotiationDeskPanel.js.map +1 -0
- package/dist/subscriberbot/components/Modal.d.ts.map +1 -1
- package/dist/subscriberbot/components/Modal.js +9 -6
- package/dist/subscriberbot/components/Modal.js.map +1 -1
- package/dist/subscriberbot/components/negotiations/NegotiationStatusPill.d.ts +8 -0
- package/dist/subscriberbot/components/negotiations/NegotiationStatusPill.d.ts.map +1 -0
- package/dist/subscriberbot/components/negotiations/NegotiationStatusPill.js +18 -0
- package/dist/subscriberbot/components/negotiations/NegotiationStatusPill.js.map +1 -0
- package/dist/subscriberbot/components/negotiations/NegotiationTimeline.d.ts +8 -0
- package/dist/subscriberbot/components/negotiations/NegotiationTimeline.d.ts.map +1 -0
- package/dist/subscriberbot/components/negotiations/NegotiationTimeline.js +49 -0
- package/dist/subscriberbot/components/negotiations/NegotiationTimeline.js.map +1 -0
- package/dist/subscriberbot/components/negotiations/OutcomeForm.d.ts +13 -0
- package/dist/subscriberbot/components/negotiations/OutcomeForm.d.ts.map +1 -0
- package/dist/subscriberbot/components/negotiations/OutcomeForm.js +112 -0
- package/dist/subscriberbot/components/negotiations/OutcomeForm.js.map +1 -0
- package/dist/subscriberbot/components/negotiations/index.d.ts +5 -0
- package/dist/subscriberbot/components/negotiations/index.d.ts.map +1 -0
- package/dist/subscriberbot/components/negotiations/index.js +4 -0
- package/dist/subscriberbot/components/negotiations/labels.d.ts +16 -0
- package/dist/subscriberbot/components/negotiations/labels.d.ts.map +1 -0
- package/dist/subscriberbot/components/negotiations/labels.js +179 -0
- package/dist/subscriberbot/components/negotiations/labels.js.map +1 -0
- package/dist/subscriberbot/hooks/useNegotiationApi.d.ts +51 -0
- package/dist/subscriberbot/hooks/useNegotiationApi.d.ts.map +1 -0
- package/dist/subscriberbot/hooks/useNegotiationApi.js +106 -0
- package/dist/subscriberbot/hooks/useNegotiationApi.js.map +1 -0
- package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts +1 -0
- package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts.map +1 -1
- package/dist/subscriberbot/hooks/useSubscriberBotApi.js +1 -1
- package/dist/subscriberbot/hooks/useSubscriberBotApi.js.map +1 -1
- package/dist/subscriberbot/pages/InvoicesPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/InvoicesPage.js +1 -0
- package/dist/subscriberbot/pages/InvoicesPage.js.map +1 -1
- package/dist/subscriberbot/pages/enterprise/EnterpriseSpendPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/enterprise/EnterpriseSpendPage.js +1 -0
- package/dist/subscriberbot/pages/enterprise/EnterpriseSpendPage.js.map +1 -1
- package/dist/subscriberbot/pages/negotiations/NegotiationDetailPage.d.ts +11 -0
- package/dist/subscriberbot/pages/negotiations/NegotiationDetailPage.d.ts.map +1 -0
- package/dist/subscriberbot/pages/negotiations/NegotiationDetailPage.js +312 -0
- package/dist/subscriberbot/pages/negotiations/NegotiationDetailPage.js.map +1 -0
- package/dist/subscriberbot/pages/negotiations/NegotiationNewPage.d.ts +11 -0
- package/dist/subscriberbot/pages/negotiations/NegotiationNewPage.d.ts.map +1 -0
- package/dist/subscriberbot/pages/negotiations/NegotiationNewPage.js +375 -0
- package/dist/subscriberbot/pages/negotiations/NegotiationNewPage.js.map +1 -0
- package/dist/subscriberbot/pages/negotiations/NegotiationsPage.d.ts +10 -0
- package/dist/subscriberbot/pages/negotiations/NegotiationsPage.d.ts.map +1 -0
- package/dist/subscriberbot/pages/negotiations/NegotiationsPage.js +155 -0
- package/dist/subscriberbot/pages/negotiations/NegotiationsPage.js.map +1 -0
- package/dist/subscriberbot/pages/providers/ProvidersListPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/providers/ProvidersListPage.js +1 -0
- package/dist/subscriberbot/pages/providers/ProvidersListPage.js.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.d.ts.map +1 -1
- package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js +321 -307
- package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js.map +1 -1
- package/dist/subscriberbot/types/negotiation.d.ts +101 -0
- package/dist/subscriberbot/types/negotiation.d.ts.map +1 -0
- package/dist/subscriberbot/utils/formatters.d.ts +1 -0
- package/dist/subscriberbot/utils/formatters.d.ts.map +1 -1
- package/dist/subscriberbot/utils/formatters.js +4 -3
- package/dist/subscriberbot/utils/formatters.js.map +1 -1
- package/package.json +2 -1
|
@@ -196,6 +196,7 @@ function M() {
|
|
|
196
196
|
/* @__PURE__ */ x(_, { className: "absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary" }),
|
|
197
197
|
/* @__PURE__ */ x("input", {
|
|
198
198
|
type: "text",
|
|
199
|
+
"aria-label": i("providers.searchLabel", "Search providers"),
|
|
199
200
|
placeholder: i("providers.searchPlaceholder", "Search providers by name…"),
|
|
200
201
|
value: M,
|
|
201
202
|
onChange: (e) => N(e.target.value),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProvidersListPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/providers/ProvidersListPage.tsx"],"sourcesContent":["/**\n * Providers list (Global Provider Registry)\n * @module subscriberbot/pages/providers\n *\n * Discovery marketplace: search and browse the provider registry. Server-side\n * search + category/status filters + pagination via `subListProviders`\n * (a SubProviderConnection). Category filter chips, a responsive grid of glass\n * provider cards with a real vendor-health indicator + churn hint, and a small\n * live summary strip computed from the fetched page.\n */\n\nimport { useState } from 'react';\nimport {\n Plus,\n Search,\n Building2,\n ShieldCheck,\n ChevronRight,\n Store,\n Activity,\n BadgeCheck,\n} from 'lucide-react';\nimport { EmphasisPanel, PagePurpose, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { MiniProgress } from '../../components';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useProviders } from '../../hooks/useSubscriberBotApi';\nimport { useTr } from '../../hooks/useTr';\nimport { cn } from '../../utils/cn';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport type { SubProvider, SubProviderCategory, SubProviderStatus } from '../../types';\n\nconst PAGE_SIZE = 24;\n\nconst CATEGORY_KEY: Record<SubProviderCategory, string> = {\n SAAS: 'providers.categories.saas',\n AI_SERVICE: 'providers.categories.aiService',\n STREAMING: 'providers.categories.streaming',\n NEWSLETTER: 'providers.categories.newsletter',\n NEWS_DIGEST: 'providers.categories.newsDigest',\n MEMBERSHIP: 'providers.categories.membership',\n COMMUNITY: 'providers.categories.community',\n EDUCATION: 'providers.categories.education',\n FINANCIAL: 'providers.categories.financial',\n INSURANCE: 'providers.categories.insurance',\n TELECOM: 'providers.categories.telecom',\n UTILITY: 'providers.categories.utility',\n ASSOCIATION: 'providers.categories.association',\n LICENSE: 'providers.categories.license',\n CONTRACT: 'providers.categories.contract',\n OTHER: 'providers.categories.other',\n};\n\nconst STATUS_KEY: Record<SubProviderStatus, string> = {\n VERIFIED: 'providers.filters.verified',\n CLAIMED: 'providers.filters.claimed',\n UNVERIFIED: 'providers.filters.unverified',\n DEPRECATED: 'providers.filters.deprecated',\n};\n\n/** Map a 0–100 vendor-health score to a MiniProgress tone. Higher is healthier. */\nfunction healthTone(score: number): 'positive' | 'accent' | 'warning' | 'negative' {\n if (score >= 70) return 'positive';\n if (score >= 45) return 'accent';\n if (score >= 25) return 'warning';\n return 'negative';\n}\n\n/** Map a 0–100 churn-risk score to a label + semantic tone. Higher is worse. */\nfunction churnMeta(score: number, tr: ReturnType<typeof useTr>): { label: string; cls: string } {\n if (score >= 67)\n return {\n label: tr('providers.card.highChurn', 'High churn risk'),\n cls: 'bg-status-error-bg-subtle text-status-error-text',\n };\n if (score >= 34)\n return {\n label: tr('providers.card.moderateChurn', 'Moderate churn risk'),\n cls: 'bg-status-warning-bg-subtle text-status-warning-text',\n };\n return {\n label: tr('providers.card.lowChurn', 'Low churn risk'),\n cls: 'bg-status-success-bg-subtle text-status-success-text',\n };\n}\n\nfunction ProviderCard({ provider, onOpen }: { provider: SubProvider; onOpen: () => void }) {\n const tr = useTr();\n const health = provider.vendorHealthScore;\n const churn = provider.churnRiskScore;\n const churnInfo = churn != null ? churnMeta(churn, tr) : null;\n\n return (\n <GlassCard\n treatment=\"glass\"\n role=\"button\"\n tabIndex={0}\n onClick={onOpen}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onOpen();\n }\n }}\n aria-label={tr('providers.card.openAria', 'Open {{name}}', { name: provider.name })}\n className=\"group flex cursor-pointer flex-col p-5 outline-none transition-all hover:-translate-y-0.5 hover:shadow-lg focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"flex size-10 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\">\n <Building2 className=\"h-5 w-5\" />\n </span>\n <StatusBadge status={provider.status} />\n </div>\n\n <p className=\"mt-3 truncate text-base font-semibold text-text-primary\">{provider.name}</p>\n <span className=\"mt-1 inline-flex w-fit rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n {tr(CATEGORY_KEY[provider.category], formatEnumLabel(provider.category))}\n </span>\n\n {provider.description ? (\n <p className=\"mt-3 line-clamp-2 min-h-[2.5rem] text-xs leading-relaxed text-text-secondary\">\n {provider.description}\n </p>\n ) : (\n <p className=\"mt-3 min-h-[2.5rem] text-xs italic text-text-secondary/70\">\n {tr('providers.card.noDescription', 'No description yet.')}\n </p>\n )}\n\n <div className=\"mt-4 border-t border-border-subtle pt-3\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"inline-flex items-center gap-1 text-text-secondary\">\n <ShieldCheck className=\"h-3.5 w-3.5\" />{' '}\n {tr('providers.card.vendorHealth', 'Vendor health')}\n </span>\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {health != null ? `${health}/100` : '—'}\n </span>\n </div>\n <MiniProgress\n value={health ?? 0}\n tone={health != null ? healthTone(health) : 'accent'}\n className=\"mt-1.5\"\n />\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center justify-between gap-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n {churnInfo && (\n <span\n className={cn(\n 'inline-flex rounded-full px-2 py-0.5 text-[11px] font-medium',\n churnInfo.cls\n )}\n >\n {churnInfo.label}\n </span>\n )}\n {provider.region && (\n <span className=\"text-[11px] text-text-secondary\">{provider.region}</span>\n )}\n </div>\n <span className=\"inline-flex items-center gap-0.5 text-xs font-medium text-text-link opacity-0 transition-opacity group-hover:opacity-100\">\n {tr('providers.card.view', 'View')} <ChevronRight className=\"h-3.5 w-3.5\" />\n </span>\n </div>\n </GlassCard>\n );\n}\n\nexport function ProvidersListPage() {\n const tr = useTr();\n const { navigateTo } = useSubscriberBot();\n const [page, setPage] = useState(0);\n const [search, setSearch] = useState('');\n const [searchInput, setSearchInput] = useState('');\n const [category, setCategory] = useState<'' | SubProviderCategory>('');\n const [status, setStatus] = useState<'' | SubProviderStatus>('');\n\n const categoryOptions: Array<{ value: '' | SubProviderCategory; label: string }> = [\n { value: '', label: tr('providers.categories.all', 'All') },\n ...Object.entries(CATEGORY_KEY).map(([value, key]) => ({\n value: value as SubProviderCategory,\n label: tr(key, formatEnumLabel(value as SubProviderCategory)),\n })),\n ];\n\n const statusOptions: Array<{ value: '' | SubProviderStatus; label: string }> = [\n { value: '', label: tr('providers.filters.anyStatus', 'Any status') },\n ...Object.entries(STATUS_KEY).map(([value, key]) => ({\n value: value as SubProviderStatus,\n label: tr(key, formatEnumLabel(value as SubProviderStatus)),\n })),\n ];\n\n const { data, isLoading, isError, refetch, isFetching } = useProviders({\n search: search || undefined,\n category: category || undefined,\n status: status || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n\n const items = data?.items ?? [];\n const hasMore = data?.pageInfo.hasMore ?? false;\n\n // Live summary strip — computed from the fetched page (never fabricated).\n const verifiedCount = items.filter((p) => p.status === 'VERIFIED').length;\n const healthScores = items.map((p) => p.vendorHealthScore).filter((s): s is number => s != null);\n const avgHealth = healthScores.length\n ? Math.round(healthScores.reduce((a, b) => a + b, 0) / healthScores.length)\n : null;\n\n const submitSearch = (e: React.FormEvent) => {\n e.preventDefault();\n setSearch(searchInput.trim());\n setPage(0);\n };\n\n const summary: Array<{ icon: typeof Store; label: string; value: string }> = [\n {\n icon: Store,\n label: tr('providers.summary.onThisPage', 'On this page'),\n value: String(items.length),\n },\n {\n icon: BadgeCheck,\n label: tr('providers.filters.verified', 'Verified'),\n value: String(verifiedCount),\n },\n {\n icon: Activity,\n label: tr('providers.summary.avgVendorHealth', 'Avg. vendor health'),\n value: avgHealth != null ? `${avgHealth}/100` : '—',\n },\n ];\n\n return (\n <PageLayout\n title={tr('providers.title', 'Discover providers')}\n description={tr(\n 'providers.description',\n 'Browse the global provider registry — the public face of the Subscription Knowledge Graph'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/providers/new')}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('providers.addProvider', 'Add provider')}\n </button>\n }\n >\n <PagePurpose className=\"mb-4\">\n {tr(\n 'providers.purpose',\n 'Every provider exists here whether or not it integrates. Search and filter to find the ones you have relationships with, compare their vendor health, or add a new one.'\n )}\n </PagePurpose>\n\n <EmphasisPanel>\n <form onSubmit={submitSearch} className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n placeholder={tr('providers.searchPlaceholder', 'Search providers by name…')}\n value={searchInput}\n onChange={(e) => setSearchInput(e.target.value)}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2.5 pl-10 pr-24 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <button\n type=\"submit\"\n className=\"absolute right-1.5 top-1/2 -translate-y-1/2 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:opacity-90\"\n >\n {tr('providers.search', 'Search')}\n </button>\n </form>\n </EmphasisPanel>\n\n {/* Category filter chips */}\n <div className=\"mt-4 flex items-center gap-2 overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\">\n {categoryOptions.map((o) => {\n const active = category === o.value;\n return (\n <button\n key={o.value || 'all'}\n type=\"button\"\n aria-pressed={active}\n onClick={() => {\n setCategory(o.value);\n setPage(0);\n }}\n className={cn(\n 'shrink-0 rounded-full border px-3.5 py-1.5 text-xs font-medium transition-colors',\n active\n ? 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle bg-bg-surface text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n {o.label}\n </button>\n );\n })}\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center gap-2\">\n <select\n aria-label={tr('providers.filters.statusLabel', 'Filter by status')}\n value={status}\n onChange={(e) => {\n setStatus(e.target.value as '' | SubProviderStatus);\n setPage(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {statusOptions.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n </div>\n\n {/* Live summary strip */}\n {!isLoading && !isError && items.length > 0 && (\n <div className=\"mt-4 grid grid-cols-1 gap-3 sm:grid-cols-3\">\n {summary.map((s) => (\n <GlassCard key={s.label} treatment=\"metric\" className=\"flex items-center gap-3 p-4\">\n <span className=\"flex size-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <s.icon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-xs text-text-secondary\">{s.label}</p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">{s.value}</p>\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n\n {isLoading ? (\n <LoadingView label={tr('providers.loading', 'Loading providers…')} />\n ) : isError ? (\n <ErrorView\n message={tr('providers.error', 'Could not load the provider registry.')}\n onRetry={() => void refetch()}\n />\n ) : items.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-search\"\n icon={<Building2 className=\"h-12 w-12\" />}\n title={tr('providers.emptyTitle', 'No providers found')}\n description={\n search\n ? tr('providers.emptyDescriptionSearch', 'No providers match \"{{search}}\".', {\n search,\n })\n : tr(\n 'providers.emptyDescription',\n 'No providers in the registry with these filters yet.'\n )\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/providers/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n {tr('providers.addProvider', 'Add provider')}\n </button>\n }\n />\n </div>\n ) : (\n <>\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {items.map((p) => (\n <ProviderCard\n key={p.id}\n provider={p}\n onOpen={() => navigateTo(`/providers/${p.id}`)}\n />\n ))}\n </div>\n\n <div className=\"mt-6 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n {tr('providers.pagination.page', 'Page {{page}}', { page: page + 1 })}\n {isFetching ? ` · ${tr('providers.pagination.refreshing', 'refreshing…')}` : ''}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => setPage((p) => Math.max(0, p - 1))}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n page === 0\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n {tr('providers.pagination.previous', 'Previous')}\n </button>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setPage((p) => p + 1)}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n !hasMore\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n {tr('providers.pagination.next', 'Next')}\n </button>\n </div>\n </div>\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ProvidersListPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/providers/ProvidersListPage.tsx"],"sourcesContent":["/**\n * Providers list (Global Provider Registry)\n * @module subscriberbot/pages/providers\n *\n * Discovery marketplace: search and browse the provider registry. Server-side\n * search + category/status filters + pagination via `subListProviders`\n * (a SubProviderConnection). Category filter chips, a responsive grid of glass\n * provider cards with a real vendor-health indicator + churn hint, and a small\n * live summary strip computed from the fetched page.\n */\n\nimport { useState } from 'react';\nimport {\n Plus,\n Search,\n Building2,\n ShieldCheck,\n ChevronRight,\n Store,\n Activity,\n BadgeCheck,\n} from 'lucide-react';\nimport { EmphasisPanel, PagePurpose, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { EmptyState } from '../../components/EmptyState';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { MiniProgress } from '../../components';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useProviders } from '../../hooks/useSubscriberBotApi';\nimport { useTr } from '../../hooks/useTr';\nimport { cn } from '../../utils/cn';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport type { SubProvider, SubProviderCategory, SubProviderStatus } from '../../types';\n\nconst PAGE_SIZE = 24;\n\nconst CATEGORY_KEY: Record<SubProviderCategory, string> = {\n SAAS: 'providers.categories.saas',\n AI_SERVICE: 'providers.categories.aiService',\n STREAMING: 'providers.categories.streaming',\n NEWSLETTER: 'providers.categories.newsletter',\n NEWS_DIGEST: 'providers.categories.newsDigest',\n MEMBERSHIP: 'providers.categories.membership',\n COMMUNITY: 'providers.categories.community',\n EDUCATION: 'providers.categories.education',\n FINANCIAL: 'providers.categories.financial',\n INSURANCE: 'providers.categories.insurance',\n TELECOM: 'providers.categories.telecom',\n UTILITY: 'providers.categories.utility',\n ASSOCIATION: 'providers.categories.association',\n LICENSE: 'providers.categories.license',\n CONTRACT: 'providers.categories.contract',\n OTHER: 'providers.categories.other',\n};\n\nconst STATUS_KEY: Record<SubProviderStatus, string> = {\n VERIFIED: 'providers.filters.verified',\n CLAIMED: 'providers.filters.claimed',\n UNVERIFIED: 'providers.filters.unverified',\n DEPRECATED: 'providers.filters.deprecated',\n};\n\n/** Map a 0–100 vendor-health score to a MiniProgress tone. Higher is healthier. */\nfunction healthTone(score: number): 'positive' | 'accent' | 'warning' | 'negative' {\n if (score >= 70) return 'positive';\n if (score >= 45) return 'accent';\n if (score >= 25) return 'warning';\n return 'negative';\n}\n\n/** Map a 0–100 churn-risk score to a label + semantic tone. Higher is worse. */\nfunction churnMeta(score: number, tr: ReturnType<typeof useTr>): { label: string; cls: string } {\n if (score >= 67)\n return {\n label: tr('providers.card.highChurn', 'High churn risk'),\n cls: 'bg-status-error-bg-subtle text-status-error-text',\n };\n if (score >= 34)\n return {\n label: tr('providers.card.moderateChurn', 'Moderate churn risk'),\n cls: 'bg-status-warning-bg-subtle text-status-warning-text',\n };\n return {\n label: tr('providers.card.lowChurn', 'Low churn risk'),\n cls: 'bg-status-success-bg-subtle text-status-success-text',\n };\n}\n\nfunction ProviderCard({ provider, onOpen }: { provider: SubProvider; onOpen: () => void }) {\n const tr = useTr();\n const health = provider.vendorHealthScore;\n const churn = provider.churnRiskScore;\n const churnInfo = churn != null ? churnMeta(churn, tr) : null;\n\n return (\n <GlassCard\n treatment=\"glass\"\n role=\"button\"\n tabIndex={0}\n onClick={onOpen}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onOpen();\n }\n }}\n aria-label={tr('providers.card.openAria', 'Open {{name}}', { name: provider.name })}\n className=\"group flex cursor-pointer flex-col p-5 outline-none transition-all hover:-translate-y-0.5 hover:shadow-lg focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]\"\n >\n <div className=\"flex items-start justify-between gap-2\">\n <span className=\"flex size-10 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\">\n <Building2 className=\"h-5 w-5\" />\n </span>\n <StatusBadge status={provider.status} />\n </div>\n\n <p className=\"mt-3 truncate text-base font-semibold text-text-primary\">{provider.name}</p>\n <span className=\"mt-1 inline-flex w-fit rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary\">\n {tr(CATEGORY_KEY[provider.category], formatEnumLabel(provider.category))}\n </span>\n\n {provider.description ? (\n <p className=\"mt-3 line-clamp-2 min-h-[2.5rem] text-xs leading-relaxed text-text-secondary\">\n {provider.description}\n </p>\n ) : (\n <p className=\"mt-3 min-h-[2.5rem] text-xs italic text-text-secondary/70\">\n {tr('providers.card.noDescription', 'No description yet.')}\n </p>\n )}\n\n <div className=\"mt-4 border-t border-border-subtle pt-3\">\n <div className=\"flex items-center justify-between text-xs\">\n <span className=\"inline-flex items-center gap-1 text-text-secondary\">\n <ShieldCheck className=\"h-3.5 w-3.5\" />{' '}\n {tr('providers.card.vendorHealth', 'Vendor health')}\n </span>\n <span className=\"font-semibold tabular-nums text-text-primary\">\n {health != null ? `${health}/100` : '—'}\n </span>\n </div>\n <MiniProgress\n value={health ?? 0}\n tone={health != null ? healthTone(health) : 'accent'}\n className=\"mt-1.5\"\n />\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center justify-between gap-2\">\n <div className=\"flex flex-wrap items-center gap-2\">\n {churnInfo && (\n <span\n className={cn(\n 'inline-flex rounded-full px-2 py-0.5 text-[11px] font-medium',\n churnInfo.cls\n )}\n >\n {churnInfo.label}\n </span>\n )}\n {provider.region && (\n <span className=\"text-[11px] text-text-secondary\">{provider.region}</span>\n )}\n </div>\n <span className=\"inline-flex items-center gap-0.5 text-xs font-medium text-text-link opacity-0 transition-opacity group-hover:opacity-100\">\n {tr('providers.card.view', 'View')} <ChevronRight className=\"h-3.5 w-3.5\" />\n </span>\n </div>\n </GlassCard>\n );\n}\n\nexport function ProvidersListPage() {\n const tr = useTr();\n const { navigateTo } = useSubscriberBot();\n const [page, setPage] = useState(0);\n const [search, setSearch] = useState('');\n const [searchInput, setSearchInput] = useState('');\n const [category, setCategory] = useState<'' | SubProviderCategory>('');\n const [status, setStatus] = useState<'' | SubProviderStatus>('');\n\n const categoryOptions: Array<{ value: '' | SubProviderCategory; label: string }> = [\n { value: '', label: tr('providers.categories.all', 'All') },\n ...Object.entries(CATEGORY_KEY).map(([value, key]) => ({\n value: value as SubProviderCategory,\n label: tr(key, formatEnumLabel(value as SubProviderCategory)),\n })),\n ];\n\n const statusOptions: Array<{ value: '' | SubProviderStatus; label: string }> = [\n { value: '', label: tr('providers.filters.anyStatus', 'Any status') },\n ...Object.entries(STATUS_KEY).map(([value, key]) => ({\n value: value as SubProviderStatus,\n label: tr(key, formatEnumLabel(value as SubProviderStatus)),\n })),\n ];\n\n const { data, isLoading, isError, refetch, isFetching } = useProviders({\n search: search || undefined,\n category: category || undefined,\n status: status || undefined,\n pagination: { limit: PAGE_SIZE, offset: page * PAGE_SIZE },\n });\n\n const items = data?.items ?? [];\n const hasMore = data?.pageInfo.hasMore ?? false;\n\n // Live summary strip — computed from the fetched page (never fabricated).\n const verifiedCount = items.filter((p) => p.status === 'VERIFIED').length;\n const healthScores = items.map((p) => p.vendorHealthScore).filter((s): s is number => s != null);\n const avgHealth = healthScores.length\n ? Math.round(healthScores.reduce((a, b) => a + b, 0) / healthScores.length)\n : null;\n\n const submitSearch = (e: React.FormEvent) => {\n e.preventDefault();\n setSearch(searchInput.trim());\n setPage(0);\n };\n\n const summary: Array<{ icon: typeof Store; label: string; value: string }> = [\n {\n icon: Store,\n label: tr('providers.summary.onThisPage', 'On this page'),\n value: String(items.length),\n },\n {\n icon: BadgeCheck,\n label: tr('providers.filters.verified', 'Verified'),\n value: String(verifiedCount),\n },\n {\n icon: Activity,\n label: tr('providers.summary.avgVendorHealth', 'Avg. vendor health'),\n value: avgHealth != null ? `${avgHealth}/100` : '—',\n },\n ];\n\n return (\n <PageLayout\n title={tr('providers.title', 'Discover providers')}\n description={tr(\n 'providers.description',\n 'Browse the global provider registry — the public face of the Subscription Knowledge Graph'\n )}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/providers/new')}\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n <Plus className=\"h-4 w-4\" /> {tr('providers.addProvider', 'Add provider')}\n </button>\n }\n >\n <PagePurpose className=\"mb-4\">\n {tr(\n 'providers.purpose',\n 'Every provider exists here whether or not it integrates. Search and filter to find the ones you have relationships with, compare their vendor health, or add a new one.'\n )}\n </PagePurpose>\n\n <EmphasisPanel>\n <form onSubmit={submitSearch} className=\"relative\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n aria-label={tr('providers.searchLabel', 'Search providers')}\n placeholder={tr('providers.searchPlaceholder', 'Search providers by name…')}\n value={searchInput}\n onChange={(e) => setSearchInput(e.target.value)}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2.5 pl-10 pr-24 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n <button\n type=\"submit\"\n className=\"absolute right-1.5 top-1/2 -translate-y-1/2 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:opacity-90\"\n >\n {tr('providers.search', 'Search')}\n </button>\n </form>\n </EmphasisPanel>\n\n {/* Category filter chips */}\n <div className=\"mt-4 flex items-center gap-2 overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden\">\n {categoryOptions.map((o) => {\n const active = category === o.value;\n return (\n <button\n key={o.value || 'all'}\n type=\"button\"\n aria-pressed={active}\n onClick={() => {\n setCategory(o.value);\n setPage(0);\n }}\n className={cn(\n 'shrink-0 rounded-full border px-3.5 py-1.5 text-xs font-medium transition-colors',\n active\n ? 'border-transparent bg-action-primary-bg text-action-primary-text'\n : 'border-border-subtle bg-bg-surface text-text-secondary hover:bg-bg-sunken hover:text-text-primary'\n )}\n >\n {o.label}\n </button>\n );\n })}\n </div>\n\n <div className=\"mt-3 flex flex-wrap items-center gap-2\">\n <select\n aria-label={tr('providers.filters.statusLabel', 'Filter by status')}\n value={status}\n onChange={(e) => {\n setStatus(e.target.value as '' | SubProviderStatus);\n setPage(0);\n }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {statusOptions.map((o) => (\n <option key={o.value} value={o.value}>\n {o.label}\n </option>\n ))}\n </select>\n </div>\n\n {/* Live summary strip */}\n {!isLoading && !isError && items.length > 0 && (\n <div className=\"mt-4 grid grid-cols-1 gap-3 sm:grid-cols-3\">\n {summary.map((s) => (\n <GlassCard key={s.label} treatment=\"metric\" className=\"flex items-center gap-3 p-4\">\n <span className=\"flex size-9 shrink-0 items-center justify-center rounded-lg bg-[var(--color-accent-soft)] text-text-primary\">\n <s.icon className=\"h-4 w-4\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"truncate text-xs text-text-secondary\">{s.label}</p>\n <p className=\"text-lg font-semibold tabular-nums text-text-primary\">{s.value}</p>\n </div>\n </GlassCard>\n ))}\n </div>\n )}\n\n {isLoading ? (\n <LoadingView label={tr('providers.loading', 'Loading providers…')} />\n ) : isError ? (\n <ErrorView\n message={tr('providers.error', 'Could not load the provider registry.')}\n onRetry={() => void refetch()}\n />\n ) : items.length === 0 ? (\n <div className=\"mt-6\">\n <EmptyState\n illustration=\"empty-search\"\n icon={<Building2 className=\"h-12 w-12\" />}\n title={tr('providers.emptyTitle', 'No providers found')}\n description={\n search\n ? tr('providers.emptyDescriptionSearch', 'No providers match \"{{search}}\".', {\n search,\n })\n : tr(\n 'providers.emptyDescription',\n 'No providers in the registry with these filters yet.'\n )\n }\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/providers/new')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n {tr('providers.addProvider', 'Add provider')}\n </button>\n }\n />\n </div>\n ) : (\n <>\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {items.map((p) => (\n <ProviderCard\n key={p.id}\n provider={p}\n onOpen={() => navigateTo(`/providers/${p.id}`)}\n />\n ))}\n </div>\n\n <div className=\"mt-6 flex items-center justify-between\">\n <p className=\"text-xs text-text-secondary\">\n {tr('providers.pagination.page', 'Page {{page}}', { page: page + 1 })}\n {isFetching ? ` · ${tr('providers.pagination.refreshing', 'refreshing…')}` : ''}\n </p>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n disabled={page === 0}\n onClick={() => setPage((p) => Math.max(0, p - 1))}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n page === 0\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n {tr('providers.pagination.previous', 'Previous')}\n </button>\n <button\n type=\"button\"\n disabled={!hasMore}\n onClick={() => setPage((p) => p + 1)}\n className={cn(\n 'rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium',\n !hasMore\n ? 'cursor-not-allowed text-text-secondary opacity-50'\n : 'text-text-primary hover:bg-bg-sunken'\n )}\n >\n {tr('providers.pagination.next', 'Next')}\n </button>\n </div>\n </div>\n </>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RelationshipDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/pages/relationships/RelationshipDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"RelationshipDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/pages/relationships/RelationshipDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAi2BH,wBAAgB,sBAAsB,gCA0kBrC"}
|