@burdenoff/microfe-store 2026.731.1 → 2026.805.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.
Files changed (35) hide show
  1. package/dist/StoreAdminRoutes.js +1 -1
  2. package/dist/StoreAdminRoutes.js.map +1 -1
  3. package/dist/StoreRoutes.js +1 -1
  4. package/dist/StoreRoutes.js.map +1 -1
  5. package/dist/components/InstallAppModal.js +211 -137
  6. package/dist/components/InstallAppModal.js.map +1 -1
  7. package/dist/generated/global-types.d.ts +6 -0
  8. package/dist/generated/global-types.js.map +1 -1
  9. package/dist/pages/AdminDashboardPage.js +1 -1
  10. package/dist/pages/AdminDashboardPage.js.map +1 -1
  11. package/dist/pages/AdminReviewModerationPage.js +1 -1
  12. package/dist/pages/AdminReviewModerationPage.js.map +1 -1
  13. package/dist/pages/AdminStoresPage.js +1 -1
  14. package/dist/pages/AdminStoresPage.js.map +1 -1
  15. package/dist/pages/AdminSubmissionsQueuePage.js +1 -1
  16. package/dist/pages/AdminSubmissionsQueuePage.js.map +1 -1
  17. package/dist/pages/AppDetailPage.js +1 -1
  18. package/dist/pages/AppDetailPage.js.map +1 -1
  19. package/dist/pages/AppSettingsPage.js +1 -1
  20. package/dist/pages/AppSettingsPage.js.map +1 -1
  21. package/dist/pages/CartPage.js +1 -1
  22. package/dist/pages/CartPage.js.map +1 -1
  23. package/dist/pages/InstalledAppsPage.js +1 -1
  24. package/dist/pages/InstalledAppsPage.js.map +1 -1
  25. package/dist/pages/MarketplaceHomePage.js +1 -1
  26. package/dist/pages/MarketplaceHomePage.js.map +1 -1
  27. package/dist/pages/MyLicensesPage.js +1 -1
  28. package/dist/pages/MyLicensesPage.js.map +1 -1
  29. package/dist/pages/OrderConfirmationPage.js +1 -1
  30. package/dist/pages/OrderConfirmationPage.js.map +1 -1
  31. package/dist/pages/OrdersPage.js +1 -1
  32. package/dist/pages/OrdersPage.js.map +1 -1
  33. package/dist/pages/ProductDetailPage.js +1 -1
  34. package/dist/pages/ProductDetailPage.js.map +1 -1
  35. package/package.json +4 -4
@@ -4,7 +4,7 @@ import { useCallback as n, useEffect as r, useState as i } from "react";
4
4
  import { ArrowRight as a, Check as o, CheckCircle as s, ChevronDown as c, ChevronUp as l, Clock as u, ExternalLink as d, LayoutDashboard as f, MessageSquareWarning as p, PackageCheck as m, RefreshCw as h, Store as g, TrendingUp as _, Users as v, X as y, XCircle as b } from "lucide-react";
5
5
  import { graphqlFetch as x } from "@burdenoff/fe-libs/shared/graphql";
6
6
  import { Fragment as S, jsx as C, jsxs as w } from "react/jsx-runtime";
7
- import { useNavigate as T } from "react-router-dom";
7
+ import { useNavigate as T } from "react-router";
8
8
  import { GlassCard as E, IllustratedEmptyState as D } from "@burdenoff/fe-libs/ui";
9
9
  //#region src/pages/AdminDashboardPage.tsx
10
10
  var O = "\n query AdminDashboardData {\n recentPending: pendingReviewProducts(limit: 5) {\n totalCount\n products {\n id\n slug\n name\n type\n nature\n category\n icon\n price\n currency\n pricingModel\n description\n submittedAt\n submittedBy\n productId\n publisher {\n name\n email\n }\n }\n }\n stores {\n id\n }\n recentPublished: products(filter: { status: PUBLISHED }, limit: 5) {\n id\n slug\n name\n icon\n type\n status\n publishedAt\n reviewedBy\n }\n recentRejected: products(filter: { status: REJECTED }, limit: 5) {\n id\n slug\n name\n icon\n type\n status\n reviewedAt\n rejectedReason\n }\n }\n", k = "\n mutation ApproveProduct($id: ID!) {\n approveProduct(id: $id) {\n id\n status\n }\n }\n", A = "\n mutation RejectProduct($id: ID!, $reason: String!) {\n rejectProduct(id: $id, reason: $reason) {\n id\n status\n }\n }\n", j = {
@@ -1 +1 @@
1
- {"version":3,"file":"AdminDashboardPage.js","names":[],"sources":["../../src/pages/AdminDashboardPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport {\n LayoutDashboard,\n PackageCheck,\n Store as StoreIcon,\n MessageSquareWarning,\n Users,\n Check,\n X,\n ExternalLink,\n CheckCircle,\n XCircle,\n RefreshCw,\n ChevronDown,\n ChevronUp,\n ArrowRight,\n Clock,\n TrendingUp,\n} from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { GlassCard, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { useNavigate } from 'react-router-dom';\nimport { useStore } from '../providers/StoreProvider';\nimport { formatPriceForModel } from '../utils';\n\ntype RecentProduct = {\n id: string;\n slug: string;\n name: string;\n type: string;\n nature?: string | null;\n category?: string | null;\n icon?: string | null;\n price: number;\n currency: string;\n pricingModel: string;\n description?: string | null;\n submittedAt?: string | null;\n submittedBy?: string | null;\n productId?: string | null;\n publisher?: { name: string; email: string } | null;\n};\n\ntype ActivityProduct = {\n id: string;\n slug: string;\n name: string;\n icon?: string | null;\n type?: string;\n status: string;\n publishedAt?: string | null;\n reviewedAt?: string | null;\n reviewedBy?: string | null;\n rejectedReason?: string | null;\n};\n\ntype DashboardState = {\n pendingCount: number;\n totalStores: number;\n recentPending: RecentProduct[];\n recentPublished: ActivityProduct[];\n recentRejected: ActivityProduct[];\n loading: boolean;\n error: string | null;\n};\n\nconst DASHBOARD_QUERY = /* GraphQL */ `\n query AdminDashboardData {\n recentPending: pendingReviewProducts(limit: 5) {\n totalCount\n products {\n id\n slug\n name\n type\n nature\n category\n icon\n price\n currency\n pricingModel\n description\n submittedAt\n submittedBy\n productId\n publisher {\n name\n email\n }\n }\n }\n stores {\n id\n }\n recentPublished: products(filter: { status: PUBLISHED }, limit: 5) {\n id\n slug\n name\n icon\n type\n status\n publishedAt\n reviewedBy\n }\n recentRejected: products(filter: { status: REJECTED }, limit: 5) {\n id\n slug\n name\n icon\n type\n status\n reviewedAt\n rejectedReason\n }\n }\n`;\n\nconst APPROVE_MUTATION = /* GraphQL */ `\n mutation ApproveProduct($id: ID!) {\n approveProduct(id: $id) {\n id\n status\n }\n }\n`;\n\nconst REJECT_MUTATION = /* GraphQL */ `\n mutation RejectProduct($id: ID!, $reason: String!) {\n rejectProduct(id: $id, reason: $reason) {\n id\n status\n }\n }\n`;\n\nconst DELIST_MUTATION = /* GraphQL */ `\n mutation DelistProduct($id: ID!, $reason: String) {\n delistProduct(id: $id, reason: $reason) {\n id\n status\n }\n }\n`;\n\nconst TYPE_COLORS: Record<string, string> = {\n NODE: 'bg-[var(--color-status-info-bg-subtle)] text-[var(--color-status-info-text)]',\n WORKFLOW: 'bg-[var(--color-status-warning-bg-subtle)] text-[var(--color-status-warning-text)]',\n INTEGRATION: 'bg-[var(--color-status-success-bg-subtle)] text-[var(--color-status-success-text)]',\n VIBEMODULE: 'bg-[var(--color-surface-accent-muted)] text-[var(--color-text-accent)]',\n};\n\nfunction typeColor(type?: string | null) {\n if (!type) return 'bg-[var(--color-surface-muted)] text-[var(--color-text-secondary)]';\n return TYPE_COLORS[type] ?? 'bg-[var(--color-surface-muted)] text-[var(--color-text-secondary)]';\n}\n\nfunction formatDate(iso?: string | null) {\n if (!iso) return '—';\n return new Date(iso).toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n });\n}\n\nfunction timeAgo(iso?: string | null) {\n if (!iso) return '—';\n const diff = Date.now() - new Date(iso).getTime();\n const mins = Math.floor(diff / 60000);\n if (mins < 60) return `${mins}m ago`;\n const hrs = Math.floor(mins / 60);\n if (hrs < 24) return `${hrs}h ago`;\n const days = Math.floor(hrs / 24);\n return `${days}d ago`;\n}\n\nexport const AdminDashboardPage: FC = () => {\n const navigate = useNavigate();\n const { authToken, basePath } = useStore();\n\n const [state, setState] = useState<DashboardState>({\n pendingCount: 0,\n totalStores: 0,\n recentPending: [],\n recentPublished: [],\n recentRejected: [],\n loading: true,\n error: null,\n });\n const [actingId, setActingId] = useState<string | null>(null);\n const [rejectingId, setRejectingId] = useState<string | null>(null);\n const [rejectReason, setRejectReason] = useState('');\n const [actionError, setActionError] = useState<string | null>(null);\n const [viewingId, setViewingId] = useState<string | null>(null);\n\n const load = useCallback(async () => {\n setState((prev) => ({ ...prev, loading: true, error: null }));\n try {\n const result = await graphqlFetch<{\n recentPending: { totalCount: number; products: RecentProduct[] };\n stores: Array<{ id: string }>;\n recentPublished: ActivityProduct[];\n recentRejected: ActivityProduct[];\n }>({\n gateway: 'global',\n query: DASHBOARD_QUERY,\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to load dashboard');\n }\n setState({\n pendingCount: result.data?.recentPending.totalCount ?? 0,\n totalStores: result.data?.stores.length ?? 0,\n recentPending: result.data?.recentPending.products ?? [],\n recentPublished: result.data?.recentPublished ?? [],\n recentRejected: result.data?.recentRejected ?? [],\n loading: false,\n error: null,\n });\n } catch (err) {\n setState((prev) => ({\n ...prev,\n loading: false,\n error: err instanceof Error ? err.message : 'Failed to load dashboard',\n }));\n }\n }, [authToken]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n const approve = async (id: string) => {\n setActingId(id);\n setActionError(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: APPROVE_MUTATION,\n variables: { id },\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Approval failed');\n }\n await load();\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Approval failed');\n } finally {\n setActingId(null);\n }\n };\n\n const reject = async (id: string) => {\n if (!rejectReason.trim()) return;\n setActingId(id);\n setActionError(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: REJECT_MUTATION,\n variables: { id, reason: rejectReason.trim() },\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Rejection failed');\n }\n setRejectingId(null);\n setRejectReason('');\n await load();\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Rejection failed');\n } finally {\n setActingId(null);\n }\n };\n\n const delist = async (id: string) => {\n setActingId(id);\n setActionError(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: DELIST_MUTATION,\n variables: { id, reason: rejectReason.trim() || undefined },\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Delist failed');\n }\n setRejectingId(null);\n setRejectReason('');\n await load();\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Delist failed');\n } finally {\n setActingId(null);\n }\n };\n\n const kpiTiles = [\n {\n label: 'Pending review',\n value: state.pendingCount,\n icon: PackageCheck,\n href: `${basePath}/submissions`,\n accent:\n state.pendingCount > 0\n ? 'border-[var(--color-status-warning-text)]/30 bg-[var(--color-status-warning-bg-subtle)]/20'\n : '',\n valueClass:\n state.pendingCount > 0\n ? 'text-[var(--color-status-warning-text)]'\n : 'text-[var(--color-text-primary)]',\n cta: 'Review queue',\n },\n {\n label: 'Stores in registry',\n value: state.totalStores,\n icon: StoreIcon,\n href: `${basePath}/stores`,\n accent: '',\n valueClass: 'text-[var(--color-text-primary)]',\n cta: 'Manage stores',\n },\n {\n label: 'Review moderation',\n value: '—',\n icon: MessageSquareWarning,\n href: `${basePath}/reviews`,\n accent: '',\n valueClass: 'text-[var(--color-text-primary)]',\n cta: 'Moderate reviews',\n },\n {\n label: 'Publishers',\n value: '—',\n icon: Users,\n href: `${basePath}/dashboard`,\n accent: '',\n valueClass: 'text-[var(--color-text-primary)]',\n cta: 'View publishers',\n },\n ];\n\n return (\n <div className=\"min-h-screen bg-[var(--color-surface-default)]\">\n {/* Header */}\n <header className=\"sticky top-0 z-10 border-b border-[var(--color-border-seam)] bg-[var(--color-surface-raised)] shadow-[var(--shadow-elevation-1)]\">\n <div className=\"mx-auto max-w-6xl px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2.5\">\n <LayoutDashboard className=\"size-5 text-[var(--color-text-primary)]\" />\n <div>\n <h1 className=\"text-lg font-semibold text-[var(--color-text-primary)]\">\n Store Admin Dashboard\n </h1>\n <p className=\"text-xs text-[var(--color-text-secondary)]\">\n Marketplace control room — submissions, registry, moderation\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => void load()}\n disabled={state.loading}\n className=\"inline-flex items-center gap-2 rounded-md border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-1.5 text-sm text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)] transition-colors disabled:opacity-50\"\n >\n <RefreshCw className={`size-4 ${state.loading ? 'animate-spin' : ''}`} />\n Refresh\n </button>\n </div>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-6xl space-y-8 p-6\">\n {state.error && (\n <div\n role=\"alert\"\n className=\"rounded-lg border border-[var(--color-border-danger)] bg-[var(--color-surface-danger-muted)] px-4 py-3 text-sm text-[var(--color-text-danger)]\"\n >\n {state.error}\n </div>\n )}\n\n {/* KPI tiles */}\n <section>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n {kpiTiles.map((tile) => {\n const Icon = tile.icon;\n return (\n <button\n key={tile.label}\n type=\"button\"\n onClick={() => navigate(tile.href)}\n className={`group relative overflow-hidden rounded-xl border border-[var(--color-border-seam)] bg-[var(--color-surface-raised)] p-5 text-left shadow-[var(--shadow-elevation-1)] transition-all hover:border-[var(--color-border-strong)] hover:shadow-[var(--shadow-elevation-2)] ${tile.accent}`}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div>\n <p className=\"text-xs font-medium text-[var(--color-text-secondary)] uppercase tracking-wide\">\n {tile.label}\n </p>\n <p className={`mt-2 text-3xl font-bold tabular-nums ${tile.valueClass}`}>\n {state.loading ? (\n <span className=\"inline-block w-8 animate-pulse rounded bg-[var(--color-surface-muted)] h-8\" />\n ) : (\n tile.value\n )}\n </p>\n </div>\n <div className=\"rounded-lg bg-[var(--color-surface-muted)] p-2\">\n <Icon className=\"size-5 text-[var(--color-text-secondary)]\" />\n </div>\n </div>\n <div className=\"mt-3 flex items-center gap-1 text-xs font-medium text-[var(--color-text-accent)] group-hover:underline\">\n {tile.cta}\n <ArrowRight className=\"size-3\" />\n </div>\n </button>\n );\n })}\n </div>\n </section>\n\n {/* Pending submissions */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <h2 className=\"text-base font-semibold text-[var(--color-text-primary)]\">\n Pending Submissions\n </h2>\n {state.pendingCount > 0 && (\n <span className=\"rounded-full bg-[var(--color-status-warning-bg-subtle)] px-2 py-0.5 text-xs font-semibold text-[var(--color-status-warning-text)]\">\n {state.pendingCount} waiting\n </span>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/submissions`)}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-1.5 text-xs font-medium text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] transition-colors\"\n >\n View all\n <ExternalLink className=\"size-3.5\" />\n </button>\n </div>\n\n {actionError && (\n <div\n role=\"alert\"\n className=\"mb-3 rounded-lg border border-[var(--color-border-danger)] bg-[var(--color-surface-danger-muted)] px-4 py-2.5 text-sm text-[var(--color-text-danger)]\"\n >\n {actionError}\n </div>\n )}\n\n {state.loading ? (\n <div className=\"space-y-3\">\n {[1, 2, 3].map((i) => (\n <div\n key={i}\n className=\"h-20 animate-pulse rounded-xl bg-[var(--color-surface-muted)]\"\n />\n ))}\n </div>\n ) : state.recentPending.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title=\"All caught up\"\n description=\"No submissions awaiting review. New products will appear here as publishers send them in.\"\n />\n ) : (\n <div className=\"overflow-hidden rounded-xl border border-[var(--color-border-seam)] bg-[var(--color-surface-raised)] shadow-[var(--shadow-elevation-1)]\">\n <ul className=\"divide-y divide-[var(--color-border-seam)]\">\n {state.recentPending.map((p) => (\n <li key={p.id} className=\"overflow-hidden\">\n <div className=\"flex items-center gap-4 px-5 py-4\">\n {p.icon ? (\n <img\n src={p.icon}\n alt=\"\"\n className=\"size-10 shrink-0 rounded-lg object-cover\"\n />\n ) : (\n <div className=\"flex size-10 shrink-0 items-center justify-center rounded-lg bg-[var(--color-surface-muted)] text-xs font-bold text-[var(--color-text-muted)] uppercase\">\n {p.type.slice(0, 2)}\n </div>\n )}\n\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n <p className=\"font-medium text-[var(--color-text-primary)] truncate\">\n {p.name}\n </p>\n <span\n className={`shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${typeColor(p.type)}`}\n >\n {p.type}\n </span>\n </div>\n <div className=\"mt-0.5 flex items-center gap-3 flex-wrap text-xs text-[var(--color-text-secondary)]\">\n <span>{p.publisher?.name ?? 'Unknown publisher'}</span>\n <span>·</span>\n <span>{formatPriceForModel(p.price, p.currency, p.pricingModel)}</span>\n {p.submittedAt && (\n <>\n <span>·</span>\n <span className=\"flex items-center gap-1\">\n <Clock className=\"size-3\" />\n {timeAgo(p.submittedAt)}\n </span>\n </>\n )}\n </div>\n </div>\n\n <div className=\"shrink-0 flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setViewingId(viewingId === p.id ? null : p.id)}\n className=\"inline-flex items-center gap-1 rounded-md border border-[var(--color-border-default)] px-2.5 py-1.5 text-xs text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] transition-colors\"\n >\n {viewingId === p.id ? (\n <ChevronUp className=\"size-3.5\" />\n ) : (\n <ChevronDown className=\"size-3.5\" />\n )}\n Details\n </button>\n <button\n type=\"button\"\n onClick={() => void approve(p.id)}\n disabled={actingId === p.id}\n className=\"inline-flex items-center gap-1 rounded-md bg-[var(--color-surface-success)] px-3 py-1.5 text-xs font-semibold text-[var(--color-text-on-success)] hover:opacity-90 disabled:opacity-50 transition-opacity\"\n >\n <Check className=\"size-3.5\" />\n Approve\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setRejectingId(p.id);\n setRejectReason('');\n setActionError(null);\n }}\n disabled={actingId === p.id}\n className=\"inline-flex items-center gap-1 rounded-md border border-[var(--color-border-danger)] bg-[var(--color-surface-danger-muted)] px-3 py-1.5 text-xs font-semibold text-[var(--color-text-danger)] hover:bg-[var(--color-surface-danger)] hover:text-[var(--color-text-on-danger)] disabled:opacity-50 transition-colors\"\n >\n <X className=\"size-3.5\" />\n Reject\n </button>\n </div>\n </div>\n\n {viewingId === p.id && (\n <div className=\"border-t border-[var(--color-border-seam)] bg-[var(--color-surface-muted)] px-5 py-4\">\n <dl className=\"grid grid-cols-2 gap-x-8 gap-y-3 text-xs sm:grid-cols-3\">\n <div>\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">Slug</dt>\n <dd className=\"mt-0.5 font-mono text-[var(--color-text-primary)]\">\n {p.slug}\n </dd>\n </div>\n <div>\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">Type</dt>\n <dd className=\"mt-0.5 text-[var(--color-text-primary)]\">\n {p.type} · {p.nature ?? '—'}\n </dd>\n </div>\n {p.category && (\n <div>\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">\n Category\n </dt>\n <dd className=\"mt-0.5 text-[var(--color-text-primary)]\">\n {p.category}\n </dd>\n </div>\n )}\n <div>\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">\n Pricing\n </dt>\n <dd className=\"mt-0.5 text-[var(--color-text-primary)]\">\n {formatPriceForModel(p.price, p.currency, p.pricingModel)}{' '}\n <span className=\"text-[var(--color-text-muted)]\">\n ({p.pricingModel})\n </span>\n </dd>\n </div>\n <div>\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">\n Publisher\n </dt>\n <dd className=\"mt-0.5 text-[var(--color-text-primary)]\">\n {p.publisher?.name ?? '—'}\n {p.publisher?.email && (\n <span className=\"block text-[var(--color-text-muted)]\">\n {p.publisher.email}\n </span>\n )}\n </dd>\n </div>\n {p.productId && (\n <div>\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">\n Devportal app\n </dt>\n <dd className=\"mt-0.5\">\n <a\n href={`/devportal/apps/${p.productId}`}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1 font-mono text-[var(--color-text-accent)] hover:underline\"\n >\n {p.productId.slice(0, 16)}…\n <ExternalLink className=\"size-3\" />\n </a>\n </dd>\n </div>\n )}\n {p.description && (\n <div className=\"col-span-2 sm:col-span-3\">\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">\n Description\n </dt>\n <dd className=\"mt-0.5 text-[var(--color-text-primary)]\">\n {p.description}\n </dd>\n </div>\n )}\n </dl>\n </div>\n )}\n\n {rejectingId === p.id && (\n <div className=\"border-t border-[var(--color-border-seam)] bg-[var(--color-surface-danger-muted)]/30 px-5 py-4\">\n <p className=\"mb-2 text-xs font-semibold text-[var(--color-text-danger)]\">\n Provide a rejection reason (the publisher will see this)\n </p>\n <textarea\n id={`reject-reason-${p.id}`}\n aria-label=\"Rejection reason\"\n value={rejectReason}\n onChange={(e) => setRejectReason(e.target.value)}\n rows={2}\n className=\"w-full rounded-lg border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-2 text-sm text-[var(--color-text-primary)] focus:border-[var(--color-border-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] resize-none\"\n placeholder=\"Explain why this submission is being rejected…\"\n />\n <div className=\"mt-2 flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setRejectingId(null);\n setRejectReason('');\n }}\n className=\"rounded-md border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-1 text-xs font-medium text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)] transition-colors\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={() => void reject(p.id)}\n disabled={!rejectReason.trim() || actingId === p.id}\n className=\"rounded-md bg-[var(--color-surface-danger)] px-3 py-1 text-xs font-semibold text-[var(--color-text-on-danger)] hover:opacity-90 disabled:opacity-50 transition-opacity\"\n >\n Submit rejection\n </button>\n </div>\n </div>\n )}\n </li>\n ))}\n </ul>\n </div>\n )}\n </section>\n\n {/* Recent Activity */}\n <section>\n <div className=\"mb-4 flex items-center gap-2\">\n <TrendingUp className=\"size-4 text-[var(--color-text-secondary)]\" />\n <h2 className=\"text-base font-semibold text-[var(--color-text-primary)]\">\n Recent Activity\n </h2>\n </div>\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n {/* Recently Approved */}\n <GlassCard className=\"overflow-hidden p-0\">\n <div className=\"flex items-center gap-2.5 border-b border-[var(--color-border-seam)] px-4 py-3\">\n <div className=\"flex size-7 items-center justify-center rounded-md bg-[var(--color-status-success-bg-subtle)]\">\n <CheckCircle className=\"size-4 text-[var(--color-status-success-text)]\" />\n </div>\n <h3 className=\"text-sm font-semibold text-[var(--color-text-primary)]\">\n Recently Approved\n </h3>\n <span className=\"ml-auto text-xs text-[var(--color-text-muted)]\">Last 5</span>\n </div>\n {state.loading ? (\n <div className=\"space-y-3 p-4\">\n {[1, 2, 3].map((i) => (\n <div\n key={i}\n className=\"h-10 animate-pulse rounded-lg bg-[var(--color-surface-muted)]\"\n />\n ))}\n </div>\n ) : state.recentPublished.length === 0 ? (\n <p className=\"px-4 py-8 text-center text-sm text-[var(--color-text-muted)]\">\n No approved products yet.\n </p>\n ) : (\n <ul className=\"divide-y divide-[var(--color-border-seam)]\">\n {state.recentPublished.map((p) => (\n <li key={p.id} className=\"flex items-center gap-3 px-4 py-3\">\n {p.icon ? (\n <img\n src={p.icon}\n alt=\"\"\n className=\"size-8 shrink-0 rounded-lg object-cover\"\n />\n ) : (\n <div className=\"flex size-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-surface-muted)] text-xs font-bold text-[var(--color-text-muted)] uppercase\">\n {(p.type ?? '?').slice(0, 2)}\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-[var(--color-text-primary)]\">\n {p.name}\n </p>\n <p className=\"text-xs text-[var(--color-text-secondary)]\">\n {p.reviewedBy ? `by ${p.reviewedBy} · ` : ''}\n {formatDate(p.publishedAt)}\n </p>\n </div>\n <span className=\"shrink-0 rounded-full bg-[var(--color-status-success-bg-subtle)] px-2.5 py-1 text-xs font-medium text-[var(--color-status-success-text)]\">\n Published\n </span>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n\n {/* Recently Rejected */}\n <GlassCard className=\"overflow-hidden p-0\">\n <div className=\"flex items-center gap-2.5 border-b border-[var(--color-border-seam)] px-4 py-3\">\n <div className=\"flex size-7 items-center justify-center rounded-md bg-[var(--color-status-error-bg-subtle)]\">\n <XCircle className=\"size-4 text-[var(--color-status-error-text)]\" />\n </div>\n <h3 className=\"text-sm font-semibold text-[var(--color-text-primary)]\">\n Recently Rejected\n </h3>\n <span className=\"ml-auto text-xs text-[var(--color-text-muted)]\">Last 5</span>\n </div>\n {state.loading ? (\n <div className=\"space-y-3 p-4\">\n {[1, 2, 3].map((i) => (\n <div\n key={i}\n className=\"h-10 animate-pulse rounded-lg bg-[var(--color-surface-muted)]\"\n />\n ))}\n </div>\n ) : state.recentRejected.length === 0 ? (\n <p className=\"px-4 py-8 text-center text-sm text-[var(--color-text-muted)]\">\n No rejected products yet.\n </p>\n ) : (\n <ul className=\"divide-y divide-[var(--color-border-seam)]\">\n {state.recentRejected.map((p) => (\n <li key={p.id} className=\"flex items-center gap-3 px-4 py-3\">\n {p.icon ? (\n <img\n src={p.icon}\n alt=\"\"\n className=\"size-8 shrink-0 rounded-lg object-cover\"\n />\n ) : (\n <div className=\"flex size-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-surface-muted)] text-xs font-bold text-[var(--color-text-muted)] uppercase\">\n {(p.type ?? '?').slice(0, 2)}\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-[var(--color-text-primary)]\">\n {p.name}\n </p>\n {p.rejectedReason && (\n <p\n className=\"truncate text-xs text-[var(--color-text-muted)]\"\n title={p.rejectedReason}\n >\n \"{p.rejectedReason}\"\n </p>\n )}\n <p className=\"text-xs text-[var(--color-text-secondary)]\">\n {formatDate(p.reviewedAt)}\n </p>\n </div>\n <span className=\"shrink-0 rounded-full bg-[var(--color-status-error-bg-subtle)] px-2.5 py-1 text-xs font-medium text-[var(--color-status-error-text)]\">\n Rejected\n </span>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n </div>\n </section>\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AdminDashboardPage.js","names":[],"sources":["../../src/pages/AdminDashboardPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport {\n LayoutDashboard,\n PackageCheck,\n Store as StoreIcon,\n MessageSquareWarning,\n Users,\n Check,\n X,\n ExternalLink,\n CheckCircle,\n XCircle,\n RefreshCw,\n ChevronDown,\n ChevronUp,\n ArrowRight,\n Clock,\n TrendingUp,\n} from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { GlassCard, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { useNavigate } from 'react-router';\nimport { useStore } from '../providers/StoreProvider';\nimport { formatPriceForModel } from '../utils';\n\ntype RecentProduct = {\n id: string;\n slug: string;\n name: string;\n type: string;\n nature?: string | null;\n category?: string | null;\n icon?: string | null;\n price: number;\n currency: string;\n pricingModel: string;\n description?: string | null;\n submittedAt?: string | null;\n submittedBy?: string | null;\n productId?: string | null;\n publisher?: { name: string; email: string } | null;\n};\n\ntype ActivityProduct = {\n id: string;\n slug: string;\n name: string;\n icon?: string | null;\n type?: string;\n status: string;\n publishedAt?: string | null;\n reviewedAt?: string | null;\n reviewedBy?: string | null;\n rejectedReason?: string | null;\n};\n\ntype DashboardState = {\n pendingCount: number;\n totalStores: number;\n recentPending: RecentProduct[];\n recentPublished: ActivityProduct[];\n recentRejected: ActivityProduct[];\n loading: boolean;\n error: string | null;\n};\n\nconst DASHBOARD_QUERY = /* GraphQL */ `\n query AdminDashboardData {\n recentPending: pendingReviewProducts(limit: 5) {\n totalCount\n products {\n id\n slug\n name\n type\n nature\n category\n icon\n price\n currency\n pricingModel\n description\n submittedAt\n submittedBy\n productId\n publisher {\n name\n email\n }\n }\n }\n stores {\n id\n }\n recentPublished: products(filter: { status: PUBLISHED }, limit: 5) {\n id\n slug\n name\n icon\n type\n status\n publishedAt\n reviewedBy\n }\n recentRejected: products(filter: { status: REJECTED }, limit: 5) {\n id\n slug\n name\n icon\n type\n status\n reviewedAt\n rejectedReason\n }\n }\n`;\n\nconst APPROVE_MUTATION = /* GraphQL */ `\n mutation ApproveProduct($id: ID!) {\n approveProduct(id: $id) {\n id\n status\n }\n }\n`;\n\nconst REJECT_MUTATION = /* GraphQL */ `\n mutation RejectProduct($id: ID!, $reason: String!) {\n rejectProduct(id: $id, reason: $reason) {\n id\n status\n }\n }\n`;\n\nconst DELIST_MUTATION = /* GraphQL */ `\n mutation DelistProduct($id: ID!, $reason: String) {\n delistProduct(id: $id, reason: $reason) {\n id\n status\n }\n }\n`;\n\nconst TYPE_COLORS: Record<string, string> = {\n NODE: 'bg-[var(--color-status-info-bg-subtle)] text-[var(--color-status-info-text)]',\n WORKFLOW: 'bg-[var(--color-status-warning-bg-subtle)] text-[var(--color-status-warning-text)]',\n INTEGRATION: 'bg-[var(--color-status-success-bg-subtle)] text-[var(--color-status-success-text)]',\n VIBEMODULE: 'bg-[var(--color-surface-accent-muted)] text-[var(--color-text-accent)]',\n};\n\nfunction typeColor(type?: string | null) {\n if (!type) return 'bg-[var(--color-surface-muted)] text-[var(--color-text-secondary)]';\n return TYPE_COLORS[type] ?? 'bg-[var(--color-surface-muted)] text-[var(--color-text-secondary)]';\n}\n\nfunction formatDate(iso?: string | null) {\n if (!iso) return '—';\n return new Date(iso).toLocaleDateString(undefined, {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n });\n}\n\nfunction timeAgo(iso?: string | null) {\n if (!iso) return '—';\n const diff = Date.now() - new Date(iso).getTime();\n const mins = Math.floor(diff / 60000);\n if (mins < 60) return `${mins}m ago`;\n const hrs = Math.floor(mins / 60);\n if (hrs < 24) return `${hrs}h ago`;\n const days = Math.floor(hrs / 24);\n return `${days}d ago`;\n}\n\nexport const AdminDashboardPage: FC = () => {\n const navigate = useNavigate();\n const { authToken, basePath } = useStore();\n\n const [state, setState] = useState<DashboardState>({\n pendingCount: 0,\n totalStores: 0,\n recentPending: [],\n recentPublished: [],\n recentRejected: [],\n loading: true,\n error: null,\n });\n const [actingId, setActingId] = useState<string | null>(null);\n const [rejectingId, setRejectingId] = useState<string | null>(null);\n const [rejectReason, setRejectReason] = useState('');\n const [actionError, setActionError] = useState<string | null>(null);\n const [viewingId, setViewingId] = useState<string | null>(null);\n\n const load = useCallback(async () => {\n setState((prev) => ({ ...prev, loading: true, error: null }));\n try {\n const result = await graphqlFetch<{\n recentPending: { totalCount: number; products: RecentProduct[] };\n stores: Array<{ id: string }>;\n recentPublished: ActivityProduct[];\n recentRejected: ActivityProduct[];\n }>({\n gateway: 'global',\n query: DASHBOARD_QUERY,\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to load dashboard');\n }\n setState({\n pendingCount: result.data?.recentPending.totalCount ?? 0,\n totalStores: result.data?.stores.length ?? 0,\n recentPending: result.data?.recentPending.products ?? [],\n recentPublished: result.data?.recentPublished ?? [],\n recentRejected: result.data?.recentRejected ?? [],\n loading: false,\n error: null,\n });\n } catch (err) {\n setState((prev) => ({\n ...prev,\n loading: false,\n error: err instanceof Error ? err.message : 'Failed to load dashboard',\n }));\n }\n }, [authToken]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n const approve = async (id: string) => {\n setActingId(id);\n setActionError(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: APPROVE_MUTATION,\n variables: { id },\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Approval failed');\n }\n await load();\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Approval failed');\n } finally {\n setActingId(null);\n }\n };\n\n const reject = async (id: string) => {\n if (!rejectReason.trim()) return;\n setActingId(id);\n setActionError(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: REJECT_MUTATION,\n variables: { id, reason: rejectReason.trim() },\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Rejection failed');\n }\n setRejectingId(null);\n setRejectReason('');\n await load();\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Rejection failed');\n } finally {\n setActingId(null);\n }\n };\n\n const delist = async (id: string) => {\n setActingId(id);\n setActionError(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: DELIST_MUTATION,\n variables: { id, reason: rejectReason.trim() || undefined },\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Delist failed');\n }\n setRejectingId(null);\n setRejectReason('');\n await load();\n } catch (err) {\n setActionError(err instanceof Error ? err.message : 'Delist failed');\n } finally {\n setActingId(null);\n }\n };\n\n const kpiTiles = [\n {\n label: 'Pending review',\n value: state.pendingCount,\n icon: PackageCheck,\n href: `${basePath}/submissions`,\n accent:\n state.pendingCount > 0\n ? 'border-[var(--color-status-warning-text)]/30 bg-[var(--color-status-warning-bg-subtle)]/20'\n : '',\n valueClass:\n state.pendingCount > 0\n ? 'text-[var(--color-status-warning-text)]'\n : 'text-[var(--color-text-primary)]',\n cta: 'Review queue',\n },\n {\n label: 'Stores in registry',\n value: state.totalStores,\n icon: StoreIcon,\n href: `${basePath}/stores`,\n accent: '',\n valueClass: 'text-[var(--color-text-primary)]',\n cta: 'Manage stores',\n },\n {\n label: 'Review moderation',\n value: '—',\n icon: MessageSquareWarning,\n href: `${basePath}/reviews`,\n accent: '',\n valueClass: 'text-[var(--color-text-primary)]',\n cta: 'Moderate reviews',\n },\n {\n label: 'Publishers',\n value: '—',\n icon: Users,\n href: `${basePath}/dashboard`,\n accent: '',\n valueClass: 'text-[var(--color-text-primary)]',\n cta: 'View publishers',\n },\n ];\n\n return (\n <div className=\"min-h-screen bg-[var(--color-surface-default)]\">\n {/* Header */}\n <header className=\"sticky top-0 z-10 border-b border-[var(--color-border-seam)] bg-[var(--color-surface-raised)] shadow-[var(--shadow-elevation-1)]\">\n <div className=\"mx-auto max-w-6xl px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2.5\">\n <LayoutDashboard className=\"size-5 text-[var(--color-text-primary)]\" />\n <div>\n <h1 className=\"text-lg font-semibold text-[var(--color-text-primary)]\">\n Store Admin Dashboard\n </h1>\n <p className=\"text-xs text-[var(--color-text-secondary)]\">\n Marketplace control room — submissions, registry, moderation\n </p>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={() => void load()}\n disabled={state.loading}\n className=\"inline-flex items-center gap-2 rounded-md border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-1.5 text-sm text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)] transition-colors disabled:opacity-50\"\n >\n <RefreshCw className={`size-4 ${state.loading ? 'animate-spin' : ''}`} />\n Refresh\n </button>\n </div>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-6xl space-y-8 p-6\">\n {state.error && (\n <div\n role=\"alert\"\n className=\"rounded-lg border border-[var(--color-border-danger)] bg-[var(--color-surface-danger-muted)] px-4 py-3 text-sm text-[var(--color-text-danger)]\"\n >\n {state.error}\n </div>\n )}\n\n {/* KPI tiles */}\n <section>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n {kpiTiles.map((tile) => {\n const Icon = tile.icon;\n return (\n <button\n key={tile.label}\n type=\"button\"\n onClick={() => navigate(tile.href)}\n className={`group relative overflow-hidden rounded-xl border border-[var(--color-border-seam)] bg-[var(--color-surface-raised)] p-5 text-left shadow-[var(--shadow-elevation-1)] transition-all hover:border-[var(--color-border-strong)] hover:shadow-[var(--shadow-elevation-2)] ${tile.accent}`}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div>\n <p className=\"text-xs font-medium text-[var(--color-text-secondary)] uppercase tracking-wide\">\n {tile.label}\n </p>\n <p className={`mt-2 text-3xl font-bold tabular-nums ${tile.valueClass}`}>\n {state.loading ? (\n <span className=\"inline-block w-8 animate-pulse rounded bg-[var(--color-surface-muted)] h-8\" />\n ) : (\n tile.value\n )}\n </p>\n </div>\n <div className=\"rounded-lg bg-[var(--color-surface-muted)] p-2\">\n <Icon className=\"size-5 text-[var(--color-text-secondary)]\" />\n </div>\n </div>\n <div className=\"mt-3 flex items-center gap-1 text-xs font-medium text-[var(--color-text-accent)] group-hover:underline\">\n {tile.cta}\n <ArrowRight className=\"size-3\" />\n </div>\n </button>\n );\n })}\n </div>\n </section>\n\n {/* Pending submissions */}\n <section>\n <div className=\"mb-4 flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <h2 className=\"text-base font-semibold text-[var(--color-text-primary)]\">\n Pending Submissions\n </h2>\n {state.pendingCount > 0 && (\n <span className=\"rounded-full bg-[var(--color-status-warning-bg-subtle)] px-2 py-0.5 text-xs font-semibold text-[var(--color-status-warning-text)]\">\n {state.pendingCount} waiting\n </span>\n )}\n </div>\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/submissions`)}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-1.5 text-xs font-medium text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] transition-colors\"\n >\n View all\n <ExternalLink className=\"size-3.5\" />\n </button>\n </div>\n\n {actionError && (\n <div\n role=\"alert\"\n className=\"mb-3 rounded-lg border border-[var(--color-border-danger)] bg-[var(--color-surface-danger-muted)] px-4 py-2.5 text-sm text-[var(--color-text-danger)]\"\n >\n {actionError}\n </div>\n )}\n\n {state.loading ? (\n <div className=\"space-y-3\">\n {[1, 2, 3].map((i) => (\n <div\n key={i}\n className=\"h-20 animate-pulse rounded-xl bg-[var(--color-surface-muted)]\"\n />\n ))}\n </div>\n ) : state.recentPending.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title=\"All caught up\"\n description=\"No submissions awaiting review. New products will appear here as publishers send them in.\"\n />\n ) : (\n <div className=\"overflow-hidden rounded-xl border border-[var(--color-border-seam)] bg-[var(--color-surface-raised)] shadow-[var(--shadow-elevation-1)]\">\n <ul className=\"divide-y divide-[var(--color-border-seam)]\">\n {state.recentPending.map((p) => (\n <li key={p.id} className=\"overflow-hidden\">\n <div className=\"flex items-center gap-4 px-5 py-4\">\n {p.icon ? (\n <img\n src={p.icon}\n alt=\"\"\n className=\"size-10 shrink-0 rounded-lg object-cover\"\n />\n ) : (\n <div className=\"flex size-10 shrink-0 items-center justify-center rounded-lg bg-[var(--color-surface-muted)] text-xs font-bold text-[var(--color-text-muted)] uppercase\">\n {p.type.slice(0, 2)}\n </div>\n )}\n\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-2 flex-wrap\">\n <p className=\"font-medium text-[var(--color-text-primary)] truncate\">\n {p.name}\n </p>\n <span\n className={`shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${typeColor(p.type)}`}\n >\n {p.type}\n </span>\n </div>\n <div className=\"mt-0.5 flex items-center gap-3 flex-wrap text-xs text-[var(--color-text-secondary)]\">\n <span>{p.publisher?.name ?? 'Unknown publisher'}</span>\n <span>·</span>\n <span>{formatPriceForModel(p.price, p.currency, p.pricingModel)}</span>\n {p.submittedAt && (\n <>\n <span>·</span>\n <span className=\"flex items-center gap-1\">\n <Clock className=\"size-3\" />\n {timeAgo(p.submittedAt)}\n </span>\n </>\n )}\n </div>\n </div>\n\n <div className=\"shrink-0 flex items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => setViewingId(viewingId === p.id ? null : p.id)}\n className=\"inline-flex items-center gap-1 rounded-md border border-[var(--color-border-default)] px-2.5 py-1.5 text-xs text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] transition-colors\"\n >\n {viewingId === p.id ? (\n <ChevronUp className=\"size-3.5\" />\n ) : (\n <ChevronDown className=\"size-3.5\" />\n )}\n Details\n </button>\n <button\n type=\"button\"\n onClick={() => void approve(p.id)}\n disabled={actingId === p.id}\n className=\"inline-flex items-center gap-1 rounded-md bg-[var(--color-surface-success)] px-3 py-1.5 text-xs font-semibold text-[var(--color-text-on-success)] hover:opacity-90 disabled:opacity-50 transition-opacity\"\n >\n <Check className=\"size-3.5\" />\n Approve\n </button>\n <button\n type=\"button\"\n onClick={() => {\n setRejectingId(p.id);\n setRejectReason('');\n setActionError(null);\n }}\n disabled={actingId === p.id}\n className=\"inline-flex items-center gap-1 rounded-md border border-[var(--color-border-danger)] bg-[var(--color-surface-danger-muted)] px-3 py-1.5 text-xs font-semibold text-[var(--color-text-danger)] hover:bg-[var(--color-surface-danger)] hover:text-[var(--color-text-on-danger)] disabled:opacity-50 transition-colors\"\n >\n <X className=\"size-3.5\" />\n Reject\n </button>\n </div>\n </div>\n\n {viewingId === p.id && (\n <div className=\"border-t border-[var(--color-border-seam)] bg-[var(--color-surface-muted)] px-5 py-4\">\n <dl className=\"grid grid-cols-2 gap-x-8 gap-y-3 text-xs sm:grid-cols-3\">\n <div>\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">Slug</dt>\n <dd className=\"mt-0.5 font-mono text-[var(--color-text-primary)]\">\n {p.slug}\n </dd>\n </div>\n <div>\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">Type</dt>\n <dd className=\"mt-0.5 text-[var(--color-text-primary)]\">\n {p.type} · {p.nature ?? '—'}\n </dd>\n </div>\n {p.category && (\n <div>\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">\n Category\n </dt>\n <dd className=\"mt-0.5 text-[var(--color-text-primary)]\">\n {p.category}\n </dd>\n </div>\n )}\n <div>\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">\n Pricing\n </dt>\n <dd className=\"mt-0.5 text-[var(--color-text-primary)]\">\n {formatPriceForModel(p.price, p.currency, p.pricingModel)}{' '}\n <span className=\"text-[var(--color-text-muted)]\">\n ({p.pricingModel})\n </span>\n </dd>\n </div>\n <div>\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">\n Publisher\n </dt>\n <dd className=\"mt-0.5 text-[var(--color-text-primary)]\">\n {p.publisher?.name ?? '—'}\n {p.publisher?.email && (\n <span className=\"block text-[var(--color-text-muted)]\">\n {p.publisher.email}\n </span>\n )}\n </dd>\n </div>\n {p.productId && (\n <div>\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">\n Devportal app\n </dt>\n <dd className=\"mt-0.5\">\n <a\n href={`/devportal/apps/${p.productId}`}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"inline-flex items-center gap-1 font-mono text-[var(--color-text-accent)] hover:underline\"\n >\n {p.productId.slice(0, 16)}…\n <ExternalLink className=\"size-3\" />\n </a>\n </dd>\n </div>\n )}\n {p.description && (\n <div className=\"col-span-2 sm:col-span-3\">\n <dt className=\"font-medium text-[var(--color-text-secondary)]\">\n Description\n </dt>\n <dd className=\"mt-0.5 text-[var(--color-text-primary)]\">\n {p.description}\n </dd>\n </div>\n )}\n </dl>\n </div>\n )}\n\n {rejectingId === p.id && (\n <div className=\"border-t border-[var(--color-border-seam)] bg-[var(--color-surface-danger-muted)]/30 px-5 py-4\">\n <p className=\"mb-2 text-xs font-semibold text-[var(--color-text-danger)]\">\n Provide a rejection reason (the publisher will see this)\n </p>\n <textarea\n id={`reject-reason-${p.id}`}\n aria-label=\"Rejection reason\"\n value={rejectReason}\n onChange={(e) => setRejectReason(e.target.value)}\n rows={2}\n className=\"w-full rounded-lg border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-2 text-sm text-[var(--color-text-primary)] focus:border-[var(--color-border-accent)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] resize-none\"\n placeholder=\"Explain why this submission is being rejected…\"\n />\n <div className=\"mt-2 flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setRejectingId(null);\n setRejectReason('');\n }}\n className=\"rounded-md border border-[var(--color-border-default)] bg-[var(--color-surface-default)] px-3 py-1 text-xs font-medium text-[var(--color-text-primary)] hover:bg-[var(--color-surface-hover)] transition-colors\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={() => void reject(p.id)}\n disabled={!rejectReason.trim() || actingId === p.id}\n className=\"rounded-md bg-[var(--color-surface-danger)] px-3 py-1 text-xs font-semibold text-[var(--color-text-on-danger)] hover:opacity-90 disabled:opacity-50 transition-opacity\"\n >\n Submit rejection\n </button>\n </div>\n </div>\n )}\n </li>\n ))}\n </ul>\n </div>\n )}\n </section>\n\n {/* Recent Activity */}\n <section>\n <div className=\"mb-4 flex items-center gap-2\">\n <TrendingUp className=\"size-4 text-[var(--color-text-secondary)]\" />\n <h2 className=\"text-base font-semibold text-[var(--color-text-primary)]\">\n Recent Activity\n </h2>\n </div>\n <div className=\"grid grid-cols-1 gap-4 lg:grid-cols-2\">\n {/* Recently Approved */}\n <GlassCard className=\"overflow-hidden p-0\">\n <div className=\"flex items-center gap-2.5 border-b border-[var(--color-border-seam)] px-4 py-3\">\n <div className=\"flex size-7 items-center justify-center rounded-md bg-[var(--color-status-success-bg-subtle)]\">\n <CheckCircle className=\"size-4 text-[var(--color-status-success-text)]\" />\n </div>\n <h3 className=\"text-sm font-semibold text-[var(--color-text-primary)]\">\n Recently Approved\n </h3>\n <span className=\"ml-auto text-xs text-[var(--color-text-muted)]\">Last 5</span>\n </div>\n {state.loading ? (\n <div className=\"space-y-3 p-4\">\n {[1, 2, 3].map((i) => (\n <div\n key={i}\n className=\"h-10 animate-pulse rounded-lg bg-[var(--color-surface-muted)]\"\n />\n ))}\n </div>\n ) : state.recentPublished.length === 0 ? (\n <p className=\"px-4 py-8 text-center text-sm text-[var(--color-text-muted)]\">\n No approved products yet.\n </p>\n ) : (\n <ul className=\"divide-y divide-[var(--color-border-seam)]\">\n {state.recentPublished.map((p) => (\n <li key={p.id} className=\"flex items-center gap-3 px-4 py-3\">\n {p.icon ? (\n <img\n src={p.icon}\n alt=\"\"\n className=\"size-8 shrink-0 rounded-lg object-cover\"\n />\n ) : (\n <div className=\"flex size-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-surface-muted)] text-xs font-bold text-[var(--color-text-muted)] uppercase\">\n {(p.type ?? '?').slice(0, 2)}\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-[var(--color-text-primary)]\">\n {p.name}\n </p>\n <p className=\"text-xs text-[var(--color-text-secondary)]\">\n {p.reviewedBy ? `by ${p.reviewedBy} · ` : ''}\n {formatDate(p.publishedAt)}\n </p>\n </div>\n <span className=\"shrink-0 rounded-full bg-[var(--color-status-success-bg-subtle)] px-2.5 py-1 text-xs font-medium text-[var(--color-status-success-text)]\">\n Published\n </span>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n\n {/* Recently Rejected */}\n <GlassCard className=\"overflow-hidden p-0\">\n <div className=\"flex items-center gap-2.5 border-b border-[var(--color-border-seam)] px-4 py-3\">\n <div className=\"flex size-7 items-center justify-center rounded-md bg-[var(--color-status-error-bg-subtle)]\">\n <XCircle className=\"size-4 text-[var(--color-status-error-text)]\" />\n </div>\n <h3 className=\"text-sm font-semibold text-[var(--color-text-primary)]\">\n Recently Rejected\n </h3>\n <span className=\"ml-auto text-xs text-[var(--color-text-muted)]\">Last 5</span>\n </div>\n {state.loading ? (\n <div className=\"space-y-3 p-4\">\n {[1, 2, 3].map((i) => (\n <div\n key={i}\n className=\"h-10 animate-pulse rounded-lg bg-[var(--color-surface-muted)]\"\n />\n ))}\n </div>\n ) : state.recentRejected.length === 0 ? (\n <p className=\"px-4 py-8 text-center text-sm text-[var(--color-text-muted)]\">\n No rejected products yet.\n </p>\n ) : (\n <ul className=\"divide-y divide-[var(--color-border-seam)]\">\n {state.recentRejected.map((p) => (\n <li key={p.id} className=\"flex items-center gap-3 px-4 py-3\">\n {p.icon ? (\n <img\n src={p.icon}\n alt=\"\"\n className=\"size-8 shrink-0 rounded-lg object-cover\"\n />\n ) : (\n <div className=\"flex size-8 shrink-0 items-center justify-center rounded-lg bg-[var(--color-surface-muted)] text-xs font-bold text-[var(--color-text-muted)] uppercase\">\n {(p.type ?? '?').slice(0, 2)}\n </div>\n )}\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-[var(--color-text-primary)]\">\n {p.name}\n </p>\n {p.rejectedReason && (\n <p\n className=\"truncate text-xs text-[var(--color-text-muted)]\"\n title={p.rejectedReason}\n >\n \"{p.rejectedReason}\"\n </p>\n )}\n <p className=\"text-xs text-[var(--color-text-secondary)]\">\n {formatDate(p.reviewedAt)}\n </p>\n </div>\n <span className=\"shrink-0 rounded-full bg-[var(--color-status-error-bg-subtle)] px-2.5 py-1 text-xs font-medium text-[var(--color-status-error-text)]\">\n Rejected\n </span>\n </li>\n ))}\n </ul>\n )}\n </GlassCard>\n </div>\n </section>\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -3,7 +3,7 @@ import { useEffect as t, useRef as n, useState as r } from "react";
3
3
  import { ArrowLeft as i, Check as a, MessageSquareWarning as o, Search as s, X as c } from "lucide-react";
4
4
  import { graphqlFetch as l } from "@burdenoff/fe-libs/shared/graphql";
5
5
  import { jsx as u, jsxs as d } from "react/jsx-runtime";
6
- import { useNavigate as f } from "react-router-dom";
6
+ import { useNavigate as f } from "react-router";
7
7
  import { GlassCard as p, IllustratedEmptyState as m, PagePurpose as h } from "@burdenoff/fe-libs/ui";
8
8
  //#region src/pages/AdminReviewModerationPage.tsx
9
9
  var g = "query SearchProductsForModeration($query: String!) { searchProducts(query: $query, limit: 10) { id name slug } }", _ = "query ProductReviewsForModeration($productId: String!) { productReviews(productId: $productId) { id userId rating title comment status helpful createdAt } }", v = "mutation RejectReview($id: ID!) { rejectReview(id: $id) { id status } }", y = "mutation ApproveReview($id: ID!) { approveReview(id: $id) { id status } }", b = () => {
@@ -1 +1 @@
1
- {"version":3,"file":"AdminReviewModerationPage.js","names":[],"sources":["../../src/pages/AdminReviewModerationPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState, useRef, useEffect } from 'react';\nimport { ArrowLeft, Search, X, Check, MessageSquareWarning } from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { GlassCard, PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { useNavigate } from 'react-router-dom';\nimport { useStore } from '../providers/StoreProvider';\n\ntype ModerationReview = {\n id: string;\n userId: string;\n rating: number;\n title?: string | null;\n comment?: string | null;\n status: string;\n helpful: number;\n createdAt: string;\n};\n\ntype ProductSearchResult = {\n id: string;\n name: string;\n slug: string;\n};\n\nconst SEARCH_PRODUCTS_QUERY = `query SearchProductsForModeration($query: String!) { searchProducts(query: $query, limit: 10) { id name slug } }`;\nconst PRODUCT_REVIEWS_QUERY = `query ProductReviewsForModeration($productId: String!) { productReviews(productId: $productId) { id userId rating title comment status helpful createdAt } }`;\nconst REJECT_REVIEW_MUTATION = `mutation RejectReview($id: ID!) { rejectReview(id: $id) { id status } }`;\nconst APPROVE_REVIEW_MUTATION = `mutation ApproveReview($id: ID!) { approveReview(id: $id) { id status } }`;\n\nexport const AdminReviewModerationPage: FC = () => {\n const navigate = useNavigate();\n const { authToken } = useStore();\n const [searchQuery, setSearchQuery] = useState('');\n const [searchResults, setSearchResults] = useState<ProductSearchResult[]>([]);\n const [searchLoading, setSearchLoading] = useState(false);\n const [showDropdown, setShowDropdown] = useState(false);\n const [selectedProduct, setSelectedProduct] = useState<ProductSearchResult | null>(null);\n const [reviews, setReviews] = useState<ModerationReview[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [actingId, setActingId] = useState<string | null>(null);\n const searchDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const handleClickOutside = (e: MouseEvent) => {\n if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {\n setShowDropdown(false);\n }\n };\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, []);\n\n const handleSearchChange = (value: string) => {\n setSearchQuery(value);\n setSelectedProduct(null);\n setReviews([]);\n setError(null);\n\n if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current);\n if (!value.trim()) {\n setSearchResults([]);\n setShowDropdown(false);\n return;\n }\n\n searchDebounceRef.current = setTimeout(async () => {\n setSearchLoading(true);\n try {\n const result = await graphqlFetch<{ searchProducts: ProductSearchResult[] }>({\n gateway: 'global',\n query: SEARCH_PRODUCTS_QUERY,\n variables: { query: value },\n authToken: authToken || undefined,\n });\n setSearchResults(result.data?.searchProducts ?? []);\n setShowDropdown(true);\n } catch {\n setSearchResults([]);\n } finally {\n setSearchLoading(false);\n }\n }, 300);\n };\n\n const selectProduct = (product: ProductSearchResult) => {\n setSelectedProduct(product);\n setSearchQuery(product.name);\n setShowDropdown(false);\n void loadReviews(product.id);\n };\n\n const loadReviews = async (productId: string) => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{ productReviews: ModerationReview[] }>({\n gateway: 'global',\n query: PRODUCT_REVIEWS_QUERY,\n variables: { productId },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to fetch reviews');\n }\n\n setReviews(result.data?.productReviews ?? []);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to fetch reviews');\n } finally {\n setLoading(false);\n }\n };\n\n const removeReview = async (reviewId: string) => {\n setActingId(reviewId);\n setError(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: REJECT_REVIEW_MUTATION,\n variables: { id: reviewId },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to remove review');\n }\n\n setReviews((prev) => prev.map((r) => (r.id === reviewId ? { ...r, status: 'REJECTED' } : r)));\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove review');\n } finally {\n setActingId(null);\n }\n };\n\n const approveReview = async (reviewId: string) => {\n setActingId(reviewId);\n setError(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: APPROVE_REVIEW_MUTATION,\n variables: { id: reviewId },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to approve review');\n }\n\n setReviews((prev) => prev.map((r) => (r.id === reviewId ? { ...r, status: 'APPROVED' } : r)));\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to approve review');\n } finally {\n setActingId(null);\n }\n };\n\n return (\n <div className=\"min-h-screen bg-surface-default\">\n <header className=\"border-b border-border-seam bg-surface-raised\">\n <div className=\"mx-auto max-w-7xl px-6 py-4\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(-1)}\n aria-label=\"Go back\"\n className=\"inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n Back\n </button>\n <div className=\"flex items-center gap-2\">\n <MessageSquareWarning className=\"size-5 text-text-primary\" />\n <h1 className=\"text-xl font-semibold text-text-primary\">Review Moderation</h1>\n </div>\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Moderate reviews by product — approve legitimate ones or remove violations\n </p>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-7xl p-6\">\n <div className=\"space-y-4\">\n <PagePurpose>\n Keep the marketplace trustworthy. Pick any product, read every review customers have\n left for it, approve legitimate ones, and remove those that break content guidelines\n (spam, abuse, or off-topic posts). Search the exact product you need to moderate rather\n than scrolling an endless global feed.\n </PagePurpose>\n\n <div ref={dropdownRef} className=\"relative\">\n <GlassCard treatment=\"glass\" glow className=\"p-4\">\n <label\n htmlFor=\"admin-review-product-search\"\n className=\"mb-1.5 block text-sm font-medium text-text-primary\"\n >\n Search product by name\n </label>\n <div className=\"relative flex items-center\">\n <Search className=\"pointer-events-none absolute left-3 size-4 text-text-secondary\" />\n <input\n id=\"admin-review-product-search\"\n aria-label=\"Search product by name\"\n value={searchQuery}\n onChange={(e) => handleSearchChange(e.target.value)}\n onFocus={() => searchResults.length > 0 && setShowDropdown(true)}\n placeholder=\"Type a product name to search...\"\n autoComplete=\"off\"\n className=\"w-full rounded-md border border-border-default bg-surface-default py-2 pl-9 pr-3 text-sm text-text-primary outline-none focus:border-border-accent\"\n />\n {searchLoading && (\n <span className=\"absolute right-3 text-xs text-text-secondary\">Searching…</span>\n )}\n </div>\n\n {selectedProduct && (\n <p className=\"mt-2 text-xs text-text-secondary\">\n Moderating:{' '}\n <span className=\"font-medium text-text-primary\">{selectedProduct.name}</span>\n <span className=\"ml-1 text-text-tertiary\">({selectedProduct.id})</span>\n </p>\n )}\n </GlassCard>\n\n {showDropdown && searchResults.length > 0 && (\n <ul className=\"absolute left-0 right-0 z-50 mt-1 rounded-md border border-border-default bg-surface-raised shadow-lg\">\n {searchResults.map((product) => (\n <li key={product.id}>\n <button\n type=\"button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => selectProduct(product)}\n className=\"w-full px-4 py-2.5 text-left text-sm hover:bg-surface-hover\"\n >\n <span className=\"font-medium text-text-primary\">{product.name}</span>\n <span className=\"ml-2 text-xs text-text-secondary\">{product.id}</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n\n {showDropdown && !searchLoading && searchResults.length === 0 && searchQuery.trim() && (\n <div className=\"absolute left-0 right-0 z-50 mt-1 rounded-md border border-border-default bg-surface-raised px-4 py-3 text-sm text-text-secondary shadow-lg\">\n No products found for &ldquo;{searchQuery}&rdquo;\n </div>\n )}\n </div>\n\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {loading && (\n <div className=\"rounded-lg border border-border-default bg-surface-raised p-10 text-center\">\n <p className=\"text-sm text-text-secondary\">Loading reviews…</p>\n </div>\n )}\n\n {!loading && !selectedProduct ? (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Search a product to moderate\"\n description=\"Use the search box above to find a product by name. Its reviews will load here so you can inspect and remove any that violate the guidelines.\"\n />\n ) : !loading && selectedProduct && reviews.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title=\"No reviews to moderate\"\n description=\"This product has no reviews right now. Nothing here needs your attention.\"\n />\n ) : (\n <ul className=\"space-y-3\">\n {reviews.map((review) => (\n <li\n key={review.id}\n className=\"rounded-lg border border-border-seam bg-surface-raised p-4 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-[var(--shadow-pop)]\"\n >\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium text-text-primary\">\n {review.title || 'Untitled review'}\n </p>\n <span\n className={`rounded-full px-2 py-0.5 text-xs font-medium ${\n review.status === 'APPROVED'\n ? 'bg-surface-success-muted text-text-success'\n : review.status === 'REJECTED'\n ? 'bg-surface-danger-muted text-text-danger'\n : 'bg-surface-warning-muted text-text-warning'\n }`}\n >\n {review.status}\n </span>\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {review.comment || 'No comment provided.'}\n </p>\n <p className=\"mt-2 text-xs text-text-secondary\">\n User {review.userId} • Rating {review.rating}/5 • Helpful {review.helpful}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {review.status !== 'APPROVED' && (\n <button\n type=\"button\"\n onClick={() => approveReview(review.id)}\n disabled={actingId === review.id}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-success px-3 py-1.5 text-sm font-medium text-text-success hover:bg-surface-success-muted disabled:cursor-not-allowed disabled:opacity-60\"\n >\n <Check className=\"size-4\" /> Approve\n </button>\n )}\n {review.status !== 'REJECTED' && (\n <button\n type=\"button\"\n onClick={() => removeReview(review.id)}\n disabled={actingId === review.id}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-danger px-3 py-1.5 text-sm font-medium text-text-danger hover:bg-surface-danger-muted disabled:cursor-not-allowed disabled:opacity-60\"\n >\n <X className=\"size-4\" /> Remove\n </button>\n )}\n </div>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AdminReviewModerationPage.js","names":[],"sources":["../../src/pages/AdminReviewModerationPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState, useRef, useEffect } from 'react';\nimport { ArrowLeft, Search, X, Check, MessageSquareWarning } from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { GlassCard, PagePurpose, IllustratedEmptyState } from '@burdenoff/fe-libs/ui';\nimport { useNavigate } from 'react-router';\nimport { useStore } from '../providers/StoreProvider';\n\ntype ModerationReview = {\n id: string;\n userId: string;\n rating: number;\n title?: string | null;\n comment?: string | null;\n status: string;\n helpful: number;\n createdAt: string;\n};\n\ntype ProductSearchResult = {\n id: string;\n name: string;\n slug: string;\n};\n\nconst SEARCH_PRODUCTS_QUERY = `query SearchProductsForModeration($query: String!) { searchProducts(query: $query, limit: 10) { id name slug } }`;\nconst PRODUCT_REVIEWS_QUERY = `query ProductReviewsForModeration($productId: String!) { productReviews(productId: $productId) { id userId rating title comment status helpful createdAt } }`;\nconst REJECT_REVIEW_MUTATION = `mutation RejectReview($id: ID!) { rejectReview(id: $id) { id status } }`;\nconst APPROVE_REVIEW_MUTATION = `mutation ApproveReview($id: ID!) { approveReview(id: $id) { id status } }`;\n\nexport const AdminReviewModerationPage: FC = () => {\n const navigate = useNavigate();\n const { authToken } = useStore();\n const [searchQuery, setSearchQuery] = useState('');\n const [searchResults, setSearchResults] = useState<ProductSearchResult[]>([]);\n const [searchLoading, setSearchLoading] = useState(false);\n const [showDropdown, setShowDropdown] = useState(false);\n const [selectedProduct, setSelectedProduct] = useState<ProductSearchResult | null>(null);\n const [reviews, setReviews] = useState<ModerationReview[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [actingId, setActingId] = useState<string | null>(null);\n const searchDebounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n useEffect(() => {\n const handleClickOutside = (e: MouseEvent) => {\n if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {\n setShowDropdown(false);\n }\n };\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, []);\n\n const handleSearchChange = (value: string) => {\n setSearchQuery(value);\n setSelectedProduct(null);\n setReviews([]);\n setError(null);\n\n if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current);\n if (!value.trim()) {\n setSearchResults([]);\n setShowDropdown(false);\n return;\n }\n\n searchDebounceRef.current = setTimeout(async () => {\n setSearchLoading(true);\n try {\n const result = await graphqlFetch<{ searchProducts: ProductSearchResult[] }>({\n gateway: 'global',\n query: SEARCH_PRODUCTS_QUERY,\n variables: { query: value },\n authToken: authToken || undefined,\n });\n setSearchResults(result.data?.searchProducts ?? []);\n setShowDropdown(true);\n } catch {\n setSearchResults([]);\n } finally {\n setSearchLoading(false);\n }\n }, 300);\n };\n\n const selectProduct = (product: ProductSearchResult) => {\n setSelectedProduct(product);\n setSearchQuery(product.name);\n setShowDropdown(false);\n void loadReviews(product.id);\n };\n\n const loadReviews = async (productId: string) => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{ productReviews: ModerationReview[] }>({\n gateway: 'global',\n query: PRODUCT_REVIEWS_QUERY,\n variables: { productId },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to fetch reviews');\n }\n\n setReviews(result.data?.productReviews ?? []);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to fetch reviews');\n } finally {\n setLoading(false);\n }\n };\n\n const removeReview = async (reviewId: string) => {\n setActingId(reviewId);\n setError(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: REJECT_REVIEW_MUTATION,\n variables: { id: reviewId },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to remove review');\n }\n\n setReviews((prev) => prev.map((r) => (r.id === reviewId ? { ...r, status: 'REJECTED' } : r)));\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to remove review');\n } finally {\n setActingId(null);\n }\n };\n\n const approveReview = async (reviewId: string) => {\n setActingId(reviewId);\n setError(null);\n try {\n const result = await graphqlFetch({\n gateway: 'global',\n query: APPROVE_REVIEW_MUTATION,\n variables: { id: reviewId },\n authToken: authToken || undefined,\n });\n\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to approve review');\n }\n\n setReviews((prev) => prev.map((r) => (r.id === reviewId ? { ...r, status: 'APPROVED' } : r)));\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to approve review');\n } finally {\n setActingId(null);\n }\n };\n\n return (\n <div className=\"min-h-screen bg-surface-default\">\n <header className=\"border-b border-border-seam bg-surface-raised\">\n <div className=\"mx-auto max-w-7xl px-6 py-4\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(-1)}\n aria-label=\"Go back\"\n className=\"inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n Back\n </button>\n <div className=\"flex items-center gap-2\">\n <MessageSquareWarning className=\"size-5 text-text-primary\" />\n <h1 className=\"text-xl font-semibold text-text-primary\">Review Moderation</h1>\n </div>\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n Moderate reviews by product — approve legitimate ones or remove violations\n </p>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-7xl p-6\">\n <div className=\"space-y-4\">\n <PagePurpose>\n Keep the marketplace trustworthy. Pick any product, read every review customers have\n left for it, approve legitimate ones, and remove those that break content guidelines\n (spam, abuse, or off-topic posts). Search the exact product you need to moderate rather\n than scrolling an endless global feed.\n </PagePurpose>\n\n <div ref={dropdownRef} className=\"relative\">\n <GlassCard treatment=\"glass\" glow className=\"p-4\">\n <label\n htmlFor=\"admin-review-product-search\"\n className=\"mb-1.5 block text-sm font-medium text-text-primary\"\n >\n Search product by name\n </label>\n <div className=\"relative flex items-center\">\n <Search className=\"pointer-events-none absolute left-3 size-4 text-text-secondary\" />\n <input\n id=\"admin-review-product-search\"\n aria-label=\"Search product by name\"\n value={searchQuery}\n onChange={(e) => handleSearchChange(e.target.value)}\n onFocus={() => searchResults.length > 0 && setShowDropdown(true)}\n placeholder=\"Type a product name to search...\"\n autoComplete=\"off\"\n className=\"w-full rounded-md border border-border-default bg-surface-default py-2 pl-9 pr-3 text-sm text-text-primary outline-none focus:border-border-accent\"\n />\n {searchLoading && (\n <span className=\"absolute right-3 text-xs text-text-secondary\">Searching…</span>\n )}\n </div>\n\n {selectedProduct && (\n <p className=\"mt-2 text-xs text-text-secondary\">\n Moderating:{' '}\n <span className=\"font-medium text-text-primary\">{selectedProduct.name}</span>\n <span className=\"ml-1 text-text-tertiary\">({selectedProduct.id})</span>\n </p>\n )}\n </GlassCard>\n\n {showDropdown && searchResults.length > 0 && (\n <ul className=\"absolute left-0 right-0 z-50 mt-1 rounded-md border border-border-default bg-surface-raised shadow-lg\">\n {searchResults.map((product) => (\n <li key={product.id}>\n <button\n type=\"button\"\n onMouseDown={(e) => e.preventDefault()}\n onClick={() => selectProduct(product)}\n className=\"w-full px-4 py-2.5 text-left text-sm hover:bg-surface-hover\"\n >\n <span className=\"font-medium text-text-primary\">{product.name}</span>\n <span className=\"ml-2 text-xs text-text-secondary\">{product.id}</span>\n </button>\n </li>\n ))}\n </ul>\n )}\n\n {showDropdown && !searchLoading && searchResults.length === 0 && searchQuery.trim() && (\n <div className=\"absolute left-0 right-0 z-50 mt-1 rounded-md border border-border-default bg-surface-raised px-4 py-3 text-sm text-text-secondary shadow-lg\">\n No products found for &ldquo;{searchQuery}&rdquo;\n </div>\n )}\n </div>\n\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {loading && (\n <div className=\"rounded-lg border border-border-default bg-surface-raised p-10 text-center\">\n <p className=\"text-sm text-text-secondary\">Loading reviews…</p>\n </div>\n )}\n\n {!loading && !selectedProduct ? (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"Search a product to moderate\"\n description=\"Use the search box above to find a product by name. Its reviews will load here so you can inspect and remove any that violate the guidelines.\"\n />\n ) : !loading && selectedProduct && reviews.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-notifications\"\n title=\"No reviews to moderate\"\n description=\"This product has no reviews right now. Nothing here needs your attention.\"\n />\n ) : (\n <ul className=\"space-y-3\">\n {reviews.map((review) => (\n <li\n key={review.id}\n className=\"rounded-lg border border-border-seam bg-surface-raised p-4 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-[var(--shadow-pop)]\"\n >\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between\">\n <div>\n <div className=\"flex items-center gap-2\">\n <p className=\"font-medium text-text-primary\">\n {review.title || 'Untitled review'}\n </p>\n <span\n className={`rounded-full px-2 py-0.5 text-xs font-medium ${\n review.status === 'APPROVED'\n ? 'bg-surface-success-muted text-text-success'\n : review.status === 'REJECTED'\n ? 'bg-surface-danger-muted text-text-danger'\n : 'bg-surface-warning-muted text-text-warning'\n }`}\n >\n {review.status}\n </span>\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {review.comment || 'No comment provided.'}\n </p>\n <p className=\"mt-2 text-xs text-text-secondary\">\n User {review.userId} • Rating {review.rating}/5 • Helpful {review.helpful}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {review.status !== 'APPROVED' && (\n <button\n type=\"button\"\n onClick={() => approveReview(review.id)}\n disabled={actingId === review.id}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-success px-3 py-1.5 text-sm font-medium text-text-success hover:bg-surface-success-muted disabled:cursor-not-allowed disabled:opacity-60\"\n >\n <Check className=\"size-4\" /> Approve\n </button>\n )}\n {review.status !== 'REJECTED' && (\n <button\n type=\"button\"\n onClick={() => removeReview(review.id)}\n disabled={actingId === review.id}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-danger px-3 py-1.5 text-sm font-medium text-text-danger hover:bg-surface-danger-muted disabled:cursor-not-allowed disabled:opacity-60\"\n >\n <X className=\"size-4\" /> Remove\n </button>\n )}\n </div>\n </div>\n </li>\n ))}\n </ul>\n )}\n </div>\n </main>\n </div>\n );\n};\n"],"mappings":"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"}
@@ -4,7 +4,7 @@ import { useCallback as n, useEffect as r, useState as i } from "react";
4
4
  import { ArrowLeft as a, Pencil as o, Plus as s, RefreshCw as c, Store as l, Trash2 as u } from "lucide-react";
5
5
  import { graphqlFetch as d } from "@burdenoff/fe-libs/shared/graphql";
6
6
  import { jsx as f, jsxs as p } from "react/jsx-runtime";
7
- import { useNavigate as m } from "react-router-dom";
7
+ import { useNavigate as m } from "react-router";
8
8
  import { AlertDialog as h, AlertDialogAction as g, AlertDialogCancel as _, AlertDialogContent as v, AlertDialogDescription as y, AlertDialogFooter as b, AlertDialogHeader as x, AlertDialogTitle as S, GlassCard as C, IllustratedEmptyState as w, NextSteps as T, PagePurpose as E } from "@burdenoff/fe-libs/ui";
9
9
  //#region src/pages/AdminStoresPage.tsx
10
10
  var D = "\n query AdminStores {\n stores {\n id\n slug\n name\n type\n tenantId\n isPublic\n requiresApproval\n allowUnverifiedPublishers\n currency\n logoUrl\n description\n createdAt\n }\n }\n", O = "\n mutation CreateStore($input: CreateStoreInput!) {\n createStore(input: $input) {\n id\n slug\n name\n type\n }\n }\n", k = "\n mutation UpdateStore($id: ID!, $input: UpdateStoreInput!) {\n updateStore(id: $id, input: $input) {\n id\n name\n isPublic\n requiresApproval\n }\n }\n", A = "\n mutation DeleteStore($id: ID!) {\n deleteStore(id: $id)\n }\n", j = {
@@ -1 +1 @@
1
- {"version":3,"file":"AdminStoresPage.js","names":[],"sources":["../../src/pages/AdminStoresPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { ArrowLeft, Plus, Pencil, Trash2, RefreshCw, Store as StoreIcon } from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { useNavigate } from 'react-router-dom';\nimport { useStore } from '../providers/StoreProvider';\nimport {\n AlertDialog,\n AlertDialogContent,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogCancel,\n AlertDialogAction,\n GlassCard,\n PagePurpose,\n IllustratedEmptyState,\n NextSteps,\n} from '@burdenoff/fe-libs/ui';\nimport { MarkdownEditor } from '../components/MarkdownEditor';\n\ntype StoreRecord = {\n id: string;\n slug: string;\n name: string;\n type: 'PUBLIC' | 'PRIVATE' | 'ENTERPRISE';\n tenantId?: string | null;\n isPublic: boolean;\n requiresApproval: boolean;\n allowUnverifiedPublishers: boolean;\n currency: string;\n logoUrl?: string | null;\n description?: string | null;\n createdAt: string;\n};\n\nconst STORES_QUERY = /* GraphQL */ `\n query AdminStores {\n stores {\n id\n slug\n name\n type\n tenantId\n isPublic\n requiresApproval\n allowUnverifiedPublishers\n currency\n logoUrl\n description\n createdAt\n }\n }\n`;\n\nconst CREATE_STORE_MUTATION = /* GraphQL */ `\n mutation CreateStore($input: CreateStoreInput!) {\n createStore(input: $input) {\n id\n slug\n name\n type\n }\n }\n`;\n\nconst UPDATE_STORE_MUTATION = /* GraphQL */ `\n mutation UpdateStore($id: ID!, $input: UpdateStoreInput!) {\n updateStore(id: $id, input: $input) {\n id\n name\n isPublic\n requiresApproval\n }\n }\n`;\n\nconst DELETE_STORE_MUTATION = /* GraphQL */ `\n mutation DeleteStore($id: ID!) {\n deleteStore(id: $id)\n }\n`;\n\ntype FormState = {\n slug: string;\n name: string;\n type: StoreRecord['type'];\n tenantId: string;\n isPublic: boolean;\n requiresApproval: boolean;\n allowUnverifiedPublishers: boolean;\n currency: string;\n description: string;\n};\n\nconst emptyForm: FormState = {\n slug: '',\n name: '',\n type: 'PRIVATE',\n tenantId: '',\n isPublic: false,\n requiresApproval: false,\n allowUnverifiedPublishers: true,\n currency: 'USD',\n description: '',\n};\n\nexport const AdminStoresPage: FC = () => {\n const navigate = useNavigate();\n const { authToken, basePath } = useStore();\n const [stores, setStores] = useState<StoreRecord[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [showForm, setShowForm] = useState(false);\n const [form, setForm] = useState<FormState>(emptyForm);\n const [editingId, setEditingId] = useState<string | null>(null);\n const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);\n\n const load = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{ stores: StoreRecord[] }>({\n gateway: 'global',\n query: STORES_QUERY,\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to load stores');\n }\n setStores(result.data?.stores ?? []);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load stores');\n } finally {\n setLoading(false);\n }\n }, [authToken]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n const openCreate = () => {\n setEditingId(null);\n setForm(emptyForm);\n setShowForm(true);\n };\n\n const openEdit = (s: StoreRecord) => {\n setEditingId(s.id);\n setForm({\n slug: s.slug,\n name: s.name,\n type: s.type,\n tenantId: s.tenantId ?? '',\n isPublic: s.isPublic,\n requiresApproval: s.requiresApproval,\n allowUnverifiedPublishers: s.allowUnverifiedPublishers,\n currency: s.currency,\n description: s.description ?? '',\n });\n setShowForm(true);\n };\n\n const submit = async () => {\n if (!form.slug.trim() || !form.name.trim()) {\n setError('Slug and name are required');\n return;\n }\n setError(null);\n try {\n if (editingId) {\n await graphqlFetch({\n gateway: 'global',\n query: UPDATE_STORE_MUTATION,\n variables: {\n id: editingId,\n input: {\n name: form.name,\n isPublic: form.isPublic,\n requiresApproval: form.requiresApproval,\n allowUnverifiedPublishers: form.allowUnverifiedPublishers,\n currency: form.currency,\n description: form.description || null,\n },\n },\n authToken: authToken || undefined,\n });\n } else {\n await graphqlFetch({\n gateway: 'global',\n query: CREATE_STORE_MUTATION,\n variables: {\n input: {\n slug: form.slug,\n name: form.name,\n type: form.type,\n tenantId: form.tenantId || null,\n isPublic: form.isPublic,\n requiresApproval: form.requiresApproval,\n allowUnverifiedPublishers: form.allowUnverifiedPublishers,\n currency: form.currency,\n description: form.description || null,\n },\n },\n authToken: authToken || undefined,\n });\n }\n setShowForm(false);\n setEditingId(null);\n setForm(emptyForm);\n await load();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Save failed');\n }\n };\n\n const remove = async (id: string) => {\n try {\n await graphqlFetch({\n gateway: 'global',\n query: DELETE_STORE_MUTATION,\n variables: { id },\n authToken: authToken || undefined,\n });\n await load();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Delete failed');\n }\n };\n\n return (\n <div className=\"min-h-screen bg-surface-default\">\n <header className=\"border-b border-border-seam bg-surface-raised\">\n <div className=\"mx-auto max-w-7xl px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/dashboard`)}\n className=\"inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n Back\n </button>\n <div className=\"flex items-center gap-2\">\n <StoreIcon className=\"size-5 text-text-primary\" />\n <h1 className=\"text-xl font-semibold text-text-primary\">Store registry</h1>\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {stores.length}\n </span>\n </div>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => void load()}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover\"\n disabled={loading}\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={openCreate}\n className=\"inline-flex items-center gap-2 rounded-md bg-surface-accent px-3 py-1.5 text-sm font-medium text-text-on-accent hover:bg-surface-accent-hover\"\n >\n <Plus className=\"size-4\" />\n Create store\n </button>\n </div>\n </div>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-7xl p-6\">\n <PagePurpose className=\"mb-4\">\n A store is a storefront that decides which products a given audience can see and buy. Use\n this registry to spin up a public marketplace, a tenant-scoped enterprise store, or a\n private opt-in catalog — and to control whether submissions auto-publish or wait for\n approval, and whether unverified publishers may submit. One store can power a whole\n product's app marketplace.\n </PagePurpose>\n\n <NextSteps\n storageKey=\"store-admin-stores\"\n steps={[\n {\n id: 'create-store',\n label: 'Create your first store',\n description: 'Choose a type (public, private, or enterprise) and a slug.',\n onClick: openCreate,\n },\n {\n id: 'approval-policy',\n label: 'Set the submission policy',\n description: 'Decide if new products require approval before going live.',\n onClick: openCreate,\n },\n {\n id: 'moderate',\n label: 'Review incoming submissions',\n description: 'Head to the submission queue to approve or reject products.',\n onClick: () => navigate(`${basePath}/submissions`),\n },\n ]}\n />\n\n {error && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {showForm && (\n <GlassCard treatment=\"plain\" className=\"mb-6 p-4\">\n <h2 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {editingId ? 'Edit store' : 'Create store'}\n </h2>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Slug</span>\n <input\n type=\"text\"\n aria-label=\"Slug\"\n value={form.slug}\n onChange={(e) => setForm({ ...form, slug: e.target.value })}\n disabled={!!editingId}\n required\n aria-required=\"true\"\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Name</span>\n <input\n type=\"text\"\n aria-label=\"Name\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n required\n aria-required=\"true\"\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Type</span>\n <select\n aria-label=\"Store type\"\n value={form.type}\n onChange={(e) => setForm({ ...form, type: e.target.value as FormState['type'] })}\n disabled={!!editingId}\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60\"\n >\n <option value=\"PUBLIC\">Public</option>\n <option value=\"PRIVATE\">Private (opt-in)</option>\n <option value=\"ENTERPRISE\">Enterprise (tenant-scoped)</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Currency</span>\n <input\n type=\"text\"\n aria-label=\"Currency\"\n value={form.currency}\n onChange={(e) => setForm({ ...form, currency: e.target.value })}\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n />\n </label>\n {form.type !== 'PUBLIC' && (\n <label className=\"block md:col-span-2\">\n <span className=\"text-xs text-text-secondary\">Tenant ID (optional)</span>\n <input\n type=\"text\"\n aria-label=\"Tenant ID\"\n value={form.tenantId}\n onChange={(e) => setForm({ ...form, tenantId: e.target.value })}\n disabled={!!editingId}\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60\"\n />\n </label>\n )}\n <MarkdownEditor\n className=\"md:col-span-2\"\n id=\"store-description\"\n label=\"Description\"\n value={form.description}\n onChange={(value) => setForm({ ...form, description: value })}\n rows={2}\n />\n <label className=\"flex items-center gap-2\">\n <input\n type=\"checkbox\"\n aria-label=\"Publicly visible\"\n checked={form.isPublic}\n onChange={(e) => setForm({ ...form, isPublic: e.target.checked })}\n />\n <span className=\"text-sm text-text-primary\">Publicly visible</span>\n </label>\n <label className=\"flex items-center gap-2\">\n <input\n type=\"checkbox\"\n aria-label=\"Requires admin approval\"\n checked={form.requiresApproval}\n onChange={(e) => setForm({ ...form, requiresApproval: e.target.checked })}\n />\n <span className=\"text-sm text-text-primary\">Requires admin approval</span>\n </label>\n <label className=\"flex items-center gap-2 md:col-span-2\">\n <input\n type=\"checkbox\"\n aria-label=\"Allow unverified publishers to submit\"\n checked={form.allowUnverifiedPublishers}\n onChange={(e) =>\n setForm({ ...form, allowUnverifiedPublishers: e.target.checked })\n }\n />\n <span className=\"text-sm text-text-primary\">\n Allow unverified publishers to submit\n </span>\n </label>\n </div>\n <div className=\"mt-4 flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setShowForm(false);\n setEditingId(null);\n setForm(emptyForm);\n }}\n className=\"rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={() => void submit()}\n className=\"rounded-md bg-surface-accent px-3 py-1.5 text-sm font-medium text-text-on-accent hover:bg-surface-accent-hover\"\n >\n {editingId ? 'Save changes' : 'Create'}\n </button>\n </div>\n </GlassCard>\n )}\n\n {loading && stores.length === 0 ? (\n <output className=\"block py-16 text-center text-text-secondary\">Loading stores…</output>\n ) : stores.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No stores yet\"\n description=\"Create your first store to give an audience a place to discover and install products.\"\n action={\n <button\n type=\"button\"\n onClick={openCreate}\n className=\"inline-flex items-center gap-2 rounded-md bg-surface-accent px-3 py-1.5 text-sm font-medium text-text-on-accent hover:bg-surface-accent-hover\"\n >\n <Plus className=\"size-4\" />\n Create store\n </button>\n }\n />\n ) : (\n <div className=\"overflow-hidden rounded-lg border border-border-seam\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-surface-muted text-left text-xs uppercase tracking-wide text-text-secondary\">\n <tr>\n <th className=\"px-4 py-2\">Slug</th>\n <th className=\"px-4 py-2\">Name</th>\n <th className=\"px-4 py-2\">Type</th>\n <th className=\"px-4 py-2\">Public</th>\n <th className=\"px-4 py-2\">Approval</th>\n <th className=\"px-4 py-2\">Currency</th>\n <th className=\"px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-seam bg-surface-raised\">\n {stores.map((s) => (\n <tr key={s.id} className=\"text-text-primary\">\n <td className=\"px-4 py-2 font-mono text-xs\">{s.slug}</td>\n <td className=\"px-4 py-2\">{s.name}</td>\n <td className=\"px-4 py-2\">\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {s.type}\n </span>\n </td>\n <td className=\"px-4 py-2 text-xs\">{s.isPublic ? 'Yes' : 'No'}</td>\n <td className=\"px-4 py-2 text-xs\">\n {s.requiresApproval ? 'Required' : 'Auto-publish'}\n </td>\n <td className=\"px-4 py-2 text-xs\">{s.currency}</td>\n <td className=\"px-4 py-2 text-right\">\n <button\n type=\"button\"\n onClick={() => openEdit(s)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-default px-2 py-1 text-xs text-text-primary hover:bg-surface-hover\"\n >\n <Pencil className=\"size-3\" />\n Edit\n </button>\n {s.id !== 'store_public_default' && (\n <button\n type=\"button\"\n onClick={() => setPendingDeleteId(s.id)}\n className=\"ml-2 inline-flex items-center gap-1 rounded-md border border-border-danger px-2 py-1 text-xs text-text-danger hover:bg-surface-danger-muted\"\n >\n <Trash2 className=\"size-3\" />\n Delete\n </button>\n )}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </main>\n\n <AlertDialog\n open={pendingDeleteId !== null}\n onOpenChange={(open) => !open && setPendingDeleteId(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Delete this store?</AlertDialogTitle>\n <AlertDialogDescription>This is a soft delete.</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel onClick={() => setPendingDeleteId(null)}>Cancel</AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-surface-danger text-text-on-danger hover:bg-surface-danger-hover\"\n onClick={() => {\n if (pendingDeleteId) void remove(pendingDeleteId);\n setPendingDeleteId(null);\n }}\n >\n Delete\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AdminStoresPage.js","names":[],"sources":["../../src/pages/AdminStoresPage.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useCallback, useEffect, useState } from 'react';\nimport { ArrowLeft, Plus, Pencil, Trash2, RefreshCw, Store as StoreIcon } from 'lucide-react';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\nimport { useNavigate } from 'react-router';\nimport { useStore } from '../providers/StoreProvider';\nimport {\n AlertDialog,\n AlertDialogContent,\n AlertDialogHeader,\n AlertDialogTitle,\n AlertDialogDescription,\n AlertDialogFooter,\n AlertDialogCancel,\n AlertDialogAction,\n GlassCard,\n PagePurpose,\n IllustratedEmptyState,\n NextSteps,\n} from '@burdenoff/fe-libs/ui';\nimport { MarkdownEditor } from '../components/MarkdownEditor';\n\ntype StoreRecord = {\n id: string;\n slug: string;\n name: string;\n type: 'PUBLIC' | 'PRIVATE' | 'ENTERPRISE';\n tenantId?: string | null;\n isPublic: boolean;\n requiresApproval: boolean;\n allowUnverifiedPublishers: boolean;\n currency: string;\n logoUrl?: string | null;\n description?: string | null;\n createdAt: string;\n};\n\nconst STORES_QUERY = /* GraphQL */ `\n query AdminStores {\n stores {\n id\n slug\n name\n type\n tenantId\n isPublic\n requiresApproval\n allowUnverifiedPublishers\n currency\n logoUrl\n description\n createdAt\n }\n }\n`;\n\nconst CREATE_STORE_MUTATION = /* GraphQL */ `\n mutation CreateStore($input: CreateStoreInput!) {\n createStore(input: $input) {\n id\n slug\n name\n type\n }\n }\n`;\n\nconst UPDATE_STORE_MUTATION = /* GraphQL */ `\n mutation UpdateStore($id: ID!, $input: UpdateStoreInput!) {\n updateStore(id: $id, input: $input) {\n id\n name\n isPublic\n requiresApproval\n }\n }\n`;\n\nconst DELETE_STORE_MUTATION = /* GraphQL */ `\n mutation DeleteStore($id: ID!) {\n deleteStore(id: $id)\n }\n`;\n\ntype FormState = {\n slug: string;\n name: string;\n type: StoreRecord['type'];\n tenantId: string;\n isPublic: boolean;\n requiresApproval: boolean;\n allowUnverifiedPublishers: boolean;\n currency: string;\n description: string;\n};\n\nconst emptyForm: FormState = {\n slug: '',\n name: '',\n type: 'PRIVATE',\n tenantId: '',\n isPublic: false,\n requiresApproval: false,\n allowUnverifiedPublishers: true,\n currency: 'USD',\n description: '',\n};\n\nexport const AdminStoresPage: FC = () => {\n const navigate = useNavigate();\n const { authToken, basePath } = useStore();\n const [stores, setStores] = useState<StoreRecord[]>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const [showForm, setShowForm] = useState(false);\n const [form, setForm] = useState<FormState>(emptyForm);\n const [editingId, setEditingId] = useState<string | null>(null);\n const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);\n\n const load = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const result = await graphqlFetch<{ stores: StoreRecord[] }>({\n gateway: 'global',\n query: STORES_QUERY,\n authToken: authToken || undefined,\n });\n if (result.errors?.length) {\n throw new Error(result.errors[0]?.message ?? 'Failed to load stores');\n }\n setStores(result.data?.stores ?? []);\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Failed to load stores');\n } finally {\n setLoading(false);\n }\n }, [authToken]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n const openCreate = () => {\n setEditingId(null);\n setForm(emptyForm);\n setShowForm(true);\n };\n\n const openEdit = (s: StoreRecord) => {\n setEditingId(s.id);\n setForm({\n slug: s.slug,\n name: s.name,\n type: s.type,\n tenantId: s.tenantId ?? '',\n isPublic: s.isPublic,\n requiresApproval: s.requiresApproval,\n allowUnverifiedPublishers: s.allowUnverifiedPublishers,\n currency: s.currency,\n description: s.description ?? '',\n });\n setShowForm(true);\n };\n\n const submit = async () => {\n if (!form.slug.trim() || !form.name.trim()) {\n setError('Slug and name are required');\n return;\n }\n setError(null);\n try {\n if (editingId) {\n await graphqlFetch({\n gateway: 'global',\n query: UPDATE_STORE_MUTATION,\n variables: {\n id: editingId,\n input: {\n name: form.name,\n isPublic: form.isPublic,\n requiresApproval: form.requiresApproval,\n allowUnverifiedPublishers: form.allowUnverifiedPublishers,\n currency: form.currency,\n description: form.description || null,\n },\n },\n authToken: authToken || undefined,\n });\n } else {\n await graphqlFetch({\n gateway: 'global',\n query: CREATE_STORE_MUTATION,\n variables: {\n input: {\n slug: form.slug,\n name: form.name,\n type: form.type,\n tenantId: form.tenantId || null,\n isPublic: form.isPublic,\n requiresApproval: form.requiresApproval,\n allowUnverifiedPublishers: form.allowUnverifiedPublishers,\n currency: form.currency,\n description: form.description || null,\n },\n },\n authToken: authToken || undefined,\n });\n }\n setShowForm(false);\n setEditingId(null);\n setForm(emptyForm);\n await load();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Save failed');\n }\n };\n\n const remove = async (id: string) => {\n try {\n await graphqlFetch({\n gateway: 'global',\n query: DELETE_STORE_MUTATION,\n variables: { id },\n authToken: authToken || undefined,\n });\n await load();\n } catch (err) {\n setError(err instanceof Error ? err.message : 'Delete failed');\n }\n };\n\n return (\n <div className=\"min-h-screen bg-surface-default\">\n <header className=\"border-b border-border-seam bg-surface-raised\">\n <div className=\"mx-auto max-w-7xl px-6 py-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <button\n type=\"button\"\n onClick={() => navigate(`${basePath}/dashboard`)}\n className=\"inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm text-text-secondary hover:bg-surface-hover hover:text-text-primary\"\n >\n <ArrowLeft className=\"size-4\" />\n Back\n </button>\n <div className=\"flex items-center gap-2\">\n <StoreIcon className=\"size-5 text-text-primary\" />\n <h1 className=\"text-xl font-semibold text-text-primary\">Store registry</h1>\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {stores.length}\n </span>\n </div>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => void load()}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover\"\n disabled={loading}\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={openCreate}\n className=\"inline-flex items-center gap-2 rounded-md bg-surface-accent px-3 py-1.5 text-sm font-medium text-text-on-accent hover:bg-surface-accent-hover\"\n >\n <Plus className=\"size-4\" />\n Create store\n </button>\n </div>\n </div>\n </div>\n </header>\n\n <main className=\"mx-auto max-w-7xl p-6\">\n <PagePurpose className=\"mb-4\">\n A store is a storefront that decides which products a given audience can see and buy. Use\n this registry to spin up a public marketplace, a tenant-scoped enterprise store, or a\n private opt-in catalog — and to control whether submissions auto-publish or wait for\n approval, and whether unverified publishers may submit. One store can power a whole\n product's app marketplace.\n </PagePurpose>\n\n <NextSteps\n storageKey=\"store-admin-stores\"\n steps={[\n {\n id: 'create-store',\n label: 'Create your first store',\n description: 'Choose a type (public, private, or enterprise) and a slug.',\n onClick: openCreate,\n },\n {\n id: 'approval-policy',\n label: 'Set the submission policy',\n description: 'Decide if new products require approval before going live.',\n onClick: openCreate,\n },\n {\n id: 'moderate',\n label: 'Review incoming submissions',\n description: 'Head to the submission queue to approve or reject products.',\n onClick: () => navigate(`${basePath}/submissions`),\n },\n ]}\n />\n\n {error && (\n <div\n role=\"alert\"\n className=\"mb-4 rounded-md border border-border-danger bg-surface-danger-muted px-4 py-3 text-sm text-text-danger\"\n >\n {error}\n </div>\n )}\n\n {showForm && (\n <GlassCard treatment=\"plain\" className=\"mb-6 p-4\">\n <h2 className=\"mb-3 text-sm font-semibold text-text-primary\">\n {editingId ? 'Edit store' : 'Create store'}\n </h2>\n <div className=\"grid grid-cols-1 gap-3 md:grid-cols-2\">\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Slug</span>\n <input\n type=\"text\"\n aria-label=\"Slug\"\n value={form.slug}\n onChange={(e) => setForm({ ...form, slug: e.target.value })}\n disabled={!!editingId}\n required\n aria-required=\"true\"\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Name</span>\n <input\n type=\"text\"\n aria-label=\"Name\"\n value={form.name}\n onChange={(e) => setForm({ ...form, name: e.target.value })}\n required\n aria-required=\"true\"\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n />\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Type</span>\n <select\n aria-label=\"Store type\"\n value={form.type}\n onChange={(e) => setForm({ ...form, type: e.target.value as FormState['type'] })}\n disabled={!!editingId}\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60\"\n >\n <option value=\"PUBLIC\">Public</option>\n <option value=\"PRIVATE\">Private (opt-in)</option>\n <option value=\"ENTERPRISE\">Enterprise (tenant-scoped)</option>\n </select>\n </label>\n <label className=\"block\">\n <span className=\"text-xs text-text-secondary\">Currency</span>\n <input\n type=\"text\"\n aria-label=\"Currency\"\n value={form.currency}\n onChange={(e) => setForm({ ...form, currency: e.target.value })}\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary\"\n />\n </label>\n {form.type !== 'PUBLIC' && (\n <label className=\"block md:col-span-2\">\n <span className=\"text-xs text-text-secondary\">Tenant ID (optional)</span>\n <input\n type=\"text\"\n aria-label=\"Tenant ID\"\n value={form.tenantId}\n onChange={(e) => setForm({ ...form, tenantId: e.target.value })}\n disabled={!!editingId}\n className=\"mt-1 w-full rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary disabled:opacity-60\"\n />\n </label>\n )}\n <MarkdownEditor\n className=\"md:col-span-2\"\n id=\"store-description\"\n label=\"Description\"\n value={form.description}\n onChange={(value) => setForm({ ...form, description: value })}\n rows={2}\n />\n <label className=\"flex items-center gap-2\">\n <input\n type=\"checkbox\"\n aria-label=\"Publicly visible\"\n checked={form.isPublic}\n onChange={(e) => setForm({ ...form, isPublic: e.target.checked })}\n />\n <span className=\"text-sm text-text-primary\">Publicly visible</span>\n </label>\n <label className=\"flex items-center gap-2\">\n <input\n type=\"checkbox\"\n aria-label=\"Requires admin approval\"\n checked={form.requiresApproval}\n onChange={(e) => setForm({ ...form, requiresApproval: e.target.checked })}\n />\n <span className=\"text-sm text-text-primary\">Requires admin approval</span>\n </label>\n <label className=\"flex items-center gap-2 md:col-span-2\">\n <input\n type=\"checkbox\"\n aria-label=\"Allow unverified publishers to submit\"\n checked={form.allowUnverifiedPublishers}\n onChange={(e) =>\n setForm({ ...form, allowUnverifiedPublishers: e.target.checked })\n }\n />\n <span className=\"text-sm text-text-primary\">\n Allow unverified publishers to submit\n </span>\n </label>\n </div>\n <div className=\"mt-4 flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => {\n setShowForm(false);\n setEditingId(null);\n setForm(emptyForm);\n }}\n className=\"rounded-md border border-border-default bg-surface-default px-3 py-1.5 text-sm text-text-primary hover:bg-surface-hover\"\n >\n Cancel\n </button>\n <button\n type=\"button\"\n onClick={() => void submit()}\n className=\"rounded-md bg-surface-accent px-3 py-1.5 text-sm font-medium text-text-on-accent hover:bg-surface-accent-hover\"\n >\n {editingId ? 'Save changes' : 'Create'}\n </button>\n </div>\n </GlassCard>\n )}\n\n {loading && stores.length === 0 ? (\n <output className=\"block py-16 text-center text-text-secondary\">Loading stores…</output>\n ) : stores.length === 0 ? (\n <IllustratedEmptyState\n illustration=\"empty-generic\"\n title=\"No stores yet\"\n description=\"Create your first store to give an audience a place to discover and install products.\"\n action={\n <button\n type=\"button\"\n onClick={openCreate}\n className=\"inline-flex items-center gap-2 rounded-md bg-surface-accent px-3 py-1.5 text-sm font-medium text-text-on-accent hover:bg-surface-accent-hover\"\n >\n <Plus className=\"size-4\" />\n Create store\n </button>\n }\n />\n ) : (\n <div className=\"overflow-hidden rounded-lg border border-border-seam\">\n <table className=\"w-full text-sm\">\n <thead className=\"bg-surface-muted text-left text-xs uppercase tracking-wide text-text-secondary\">\n <tr>\n <th className=\"px-4 py-2\">Slug</th>\n <th className=\"px-4 py-2\">Name</th>\n <th className=\"px-4 py-2\">Type</th>\n <th className=\"px-4 py-2\">Public</th>\n <th className=\"px-4 py-2\">Approval</th>\n <th className=\"px-4 py-2\">Currency</th>\n <th className=\"px-4 py-2 text-right\">Actions</th>\n </tr>\n </thead>\n <tbody className=\"divide-y divide-border-seam bg-surface-raised\">\n {stores.map((s) => (\n <tr key={s.id} className=\"text-text-primary\">\n <td className=\"px-4 py-2 font-mono text-xs\">{s.slug}</td>\n <td className=\"px-4 py-2\">{s.name}</td>\n <td className=\"px-4 py-2\">\n <span className=\"rounded-full bg-surface-accent-muted px-2 py-0.5 text-xs font-medium text-text-accent\">\n {s.type}\n </span>\n </td>\n <td className=\"px-4 py-2 text-xs\">{s.isPublic ? 'Yes' : 'No'}</td>\n <td className=\"px-4 py-2 text-xs\">\n {s.requiresApproval ? 'Required' : 'Auto-publish'}\n </td>\n <td className=\"px-4 py-2 text-xs\">{s.currency}</td>\n <td className=\"px-4 py-2 text-right\">\n <button\n type=\"button\"\n onClick={() => openEdit(s)}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-default px-2 py-1 text-xs text-text-primary hover:bg-surface-hover\"\n >\n <Pencil className=\"size-3\" />\n Edit\n </button>\n {s.id !== 'store_public_default' && (\n <button\n type=\"button\"\n onClick={() => setPendingDeleteId(s.id)}\n className=\"ml-2 inline-flex items-center gap-1 rounded-md border border-border-danger px-2 py-1 text-xs text-text-danger hover:bg-surface-danger-muted\"\n >\n <Trash2 className=\"size-3\" />\n Delete\n </button>\n )}\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )}\n </main>\n\n <AlertDialog\n open={pendingDeleteId !== null}\n onOpenChange={(open) => !open && setPendingDeleteId(null)}\n >\n <AlertDialogContent>\n <AlertDialogHeader>\n <AlertDialogTitle>Delete this store?</AlertDialogTitle>\n <AlertDialogDescription>This is a soft delete.</AlertDialogDescription>\n </AlertDialogHeader>\n <AlertDialogFooter>\n <AlertDialogCancel onClick={() => setPendingDeleteId(null)}>Cancel</AlertDialogCancel>\n <AlertDialogAction\n className=\"bg-surface-danger text-text-on-danger hover:bg-surface-danger-hover\"\n onClick={() => {\n if (pendingDeleteId) void remove(pendingDeleteId);\n setPendingDeleteId(null);\n }}\n >\n Delete\n </AlertDialogAction>\n </AlertDialogFooter>\n </AlertDialogContent>\n </AlertDialog>\n </div>\n );\n};\n"],"mappings":";;;;;;;;;AAqCA,IAAM,IAA6B,iQAmB7B,IAAsC,uJAWtC,IAAsC,0LAWtC,IAAsC,yEAkBtC,IAAuB;CAC3B,MAAM;CACN,MAAM;CACN,MAAM;CACN,UAAU;CACV,UAAU;CACV,kBAAkB;CAClB,2BAA2B;CAC3B,UAAU;CACV,aAAa;CACd,EAEY,UAA4B;CACvC,IAAM,IAAW,GAAa,EACxB,EAAE,cAAW,gBAAa,GAAU,EACpC,CAAC,GAAQ,KAAa,EAAwB,EAAE,CAAC,EACjD,CAAC,GAAS,KAAc,EAAS,GAAM,EACvC,CAAC,GAAO,KAAY,EAAwB,KAAK,EACjD,CAAC,GAAU,KAAe,EAAS,GAAM,EACzC,CAAC,GAAM,KAAW,EAAoB,EAAU,EAChD,CAAC,GAAW,KAAgB,EAAwB,KAAK,EACzD,CAAC,GAAiB,KAAsB,EAAwB,KAAK,EAErE,IAAO,EAAY,YAAY;AAEnC,EADA,EAAW,GAAK,EAChB,EAAS,KAAK;AACd,MAAI;GACF,IAAM,IAAS,MAAM,EAAwC;IAC3D,SAAS;IACT,OAAO;IACP,WAAW,KAAa,KAAA;IACzB,CAAC;AACF,OAAI,EAAO,QAAQ,OACjB,OAAU,MAAM,EAAO,OAAO,IAAI,WAAW,wBAAwB;AAEvE,KAAU,EAAO,MAAM,UAAU,EAAE,CAAC;WAC7B,GAAK;AACZ,KAAS,aAAe,QAAQ,EAAI,UAAU,wBAAwB;YAC9D;AACR,KAAW,GAAM;;IAElB,CAAC,EAAU,CAAC;AAEf,SAAgB;AACT,KAAM;IACV,CAAC,EAAK,CAAC;CAEV,IAAM,UAAmB;AAGvB,EAFA,EAAa,KAAK,EAClB,EAAQ,EAAU,EAClB,EAAY,GAAK;IAGb,KAAY,MAAmB;AAanC,EAZA,EAAa,EAAE,GAAG,EAClB,EAAQ;GACN,MAAM,EAAE;GACR,MAAM,EAAE;GACR,MAAM,EAAE;GACR,UAAU,EAAE,YAAY;GACxB,UAAU,EAAE;GACZ,kBAAkB,EAAE;GACpB,2BAA2B,EAAE;GAC7B,UAAU,EAAE;GACZ,aAAa,EAAE,eAAe;GAC/B,CAAC,EACF,EAAY,GAAK;IAGb,IAAS,YAAY;AACzB,MAAI,CAAC,EAAK,KAAK,MAAM,IAAI,CAAC,EAAK,KAAK,MAAM,EAAE;AAC1C,KAAS,6BAA6B;AACtC;;AAEF,IAAS,KAAK;AACd,MAAI;AAyCF,GAxCI,IACF,MAAM,EAAa;IACjB,SAAS;IACT,OAAO;IACP,WAAW;KACT,IAAI;KACJ,OAAO;MACL,MAAM,EAAK;MACX,UAAU,EAAK;MACf,kBAAkB,EAAK;MACvB,2BAA2B,EAAK;MAChC,UAAU,EAAK;MACf,aAAa,EAAK,eAAe;MAClC;KACF;IACD,WAAW,KAAa,KAAA;IACzB,CAAC,GAEF,MAAM,EAAa;IACjB,SAAS;IACT,OAAO;IACP,WAAW,EACT,OAAO;KACL,MAAM,EAAK;KACX,MAAM,EAAK;KACX,MAAM,EAAK;KACX,UAAU,EAAK,YAAY;KAC3B,UAAU,EAAK;KACf,kBAAkB,EAAK;KACvB,2BAA2B,EAAK;KAChC,UAAU,EAAK;KACf,aAAa,EAAK,eAAe;KAClC,EACF;IACD,WAAW,KAAa,KAAA;IACzB,CAAC,EAEJ,EAAY,GAAM,EAClB,EAAa,KAAK,EAClB,EAAQ,EAAU,EAClB,MAAM,GAAM;WACL,GAAK;AACZ,KAAS,aAAe,QAAQ,EAAI,UAAU,cAAc;;IAI1D,IAAS,OAAO,MAAe;AACnC,MAAI;AAOF,GANA,MAAM,EAAa;IACjB,SAAS;IACT,OAAO;IACP,WAAW,EAAE,OAAI;IACjB,WAAW,KAAa,KAAA;IACzB,CAAC,EACF,MAAM,GAAM;WACL,GAAK;AACZ,KAAS,aAAe,QAAQ,EAAI,UAAU,gBAAgB;;;AAIlE,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,UAAD;IAAQ,WAAU;cAChB,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,EAAS,GAAG,EAAS,YAAY;QAChD,WAAU;kBAHZ,CAKE,kBAAC,GAAD,EAAW,WAAU,UAAW,CAAA,EAAA,OAEzB;WACT,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,GAAD,EAAW,WAAU,4BAA6B,CAAA;SAClD,kBAAC,MAAD;UAAI,WAAU;oBAA0C;UAAmB,CAAA;SAC3E,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAO;UACH,CAAA;SACH;UACF;UACN,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,UAAD;QACE,MAAK;QACL,eAAe,KAAK,GAAM;QAC1B,WAAU;QACV,UAAU;kBAJZ,CAME,kBAAC,GAAD,EAAW,WAAW,UAAU,IAAU,iBAAiB,MAAQ,CAAA,EAAA,UAE5D;WACT,kBAAC,UAAD;QACE,MAAK;QACL,SAAS;QACT,WAAU;kBAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,EAAA,eAEpB;UACL;SACF;;KACF,CAAA;IACC,CAAA;GAET,kBAAC,QAAD;IAAM,WAAU;cAAhB;KACE,kBAAC,GAAD;MAAa,WAAU;gBAAO;MAMhB,CAAA;KAEd,kBAAC,GAAD;MACE,YAAW;MACX,OAAO;OACL;QACE,IAAI;QACJ,OAAO;QACP,aAAa;QACb,SAAS;QACV;OACD;QACE,IAAI;QACJ,OAAO;QACP,aAAa;QACb,SAAS;QACV;OACD;QACE,IAAI;QACJ,OAAO;QACP,aAAa;QACb,eAAe,EAAS,GAAG,EAAS,cAAc;QACnD;OACF;MACD,CAAA;KAED,KACC,kBAAC,OAAD;MACE,MAAK;MACL,WAAU;gBAET;MACG,CAAA;KAGP,KACC,kBAAC,GAAD;MAAW,WAAU;MAAQ,WAAU;gBAAvC;OACE,kBAAC,MAAD;QAAI,WAAU;kBACX,IAAY,eAAe;QACzB,CAAA;OACL,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,QAAD;WAAM,WAAU;qBAA8B;WAAW,CAAA,EACzD,kBAAC,SAAD;WACE,MAAK;WACL,cAAW;WACX,OAAO,EAAK;WACZ,WAAW,MAAM,EAAQ;YAAE,GAAG;YAAM,MAAM,EAAE,OAAO;YAAO,CAAC;WAC3D,UAAU,CAAC,CAAC;WACZ,UAAA;WACA,iBAAc;WACd,WAAU;WACV,CAAA,CACI;;SACR,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,QAAD;WAAM,WAAU;qBAA8B;WAAW,CAAA,EACzD,kBAAC,SAAD;WACE,MAAK;WACL,cAAW;WACX,OAAO,EAAK;WACZ,WAAW,MAAM,EAAQ;YAAE,GAAG;YAAM,MAAM,EAAE,OAAO;YAAO,CAAC;WAC3D,UAAA;WACA,iBAAc;WACd,WAAU;WACV,CAAA,CACI;;SACR,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,QAAD;WAAM,WAAU;qBAA8B;WAAW,CAAA,EACzD,kBAAC,UAAD;WACE,cAAW;WACX,OAAO,EAAK;WACZ,WAAW,MAAM,EAAQ;YAAE,GAAG;YAAM,MAAM,EAAE,OAAO;YAA4B,CAAC;WAChF,UAAU,CAAC,CAAC;WACZ,WAAU;qBALZ;YAOE,kBAAC,UAAD;aAAQ,OAAM;uBAAS;aAAe,CAAA;YACtC,kBAAC,UAAD;aAAQ,OAAM;uBAAU;aAAyB,CAAA;YACjD,kBAAC,UAAD;aAAQ,OAAM;uBAAa;aAAmC,CAAA;YACvD;aACH;;SACR,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,QAAD;WAAM,WAAU;qBAA8B;WAAe,CAAA,EAC7D,kBAAC,SAAD;WACE,MAAK;WACL,cAAW;WACX,OAAO,EAAK;WACZ,WAAW,MAAM,EAAQ;YAAE,GAAG;YAAM,UAAU,EAAE,OAAO;YAAO,CAAC;WAC/D,WAAU;WACV,CAAA,CACI;;SACP,EAAK,SAAS,YACb,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,QAAD;WAAM,WAAU;qBAA8B;WAA2B,CAAA,EACzE,kBAAC,SAAD;WACE,MAAK;WACL,cAAW;WACX,OAAO,EAAK;WACZ,WAAW,MAAM,EAAQ;YAAE,GAAG;YAAM,UAAU,EAAE,OAAO;YAAO,CAAC;WAC/D,UAAU,CAAC,CAAC;WACZ,WAAU;WACV,CAAA,CACI;;SAEV,kBAAC,GAAD;UACE,WAAU;UACV,IAAG;UACH,OAAM;UACN,OAAO,EAAK;UACZ,WAAW,MAAU,EAAQ;WAAE,GAAG;WAAM,aAAa;WAAO,CAAC;UAC7D,MAAM;UACN,CAAA;SACF,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,SAAD;WACE,MAAK;WACL,cAAW;WACX,SAAS,EAAK;WACd,WAAW,MAAM,EAAQ;YAAE,GAAG;YAAM,UAAU,EAAE,OAAO;YAAS,CAAC;WACjE,CAAA,EACF,kBAAC,QAAD;WAAM,WAAU;qBAA4B;WAAuB,CAAA,CAC7D;;SACR,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,SAAD;WACE,MAAK;WACL,cAAW;WACX,SAAS,EAAK;WACd,WAAW,MAAM,EAAQ;YAAE,GAAG;YAAM,kBAAkB,EAAE,OAAO;YAAS,CAAC;WACzE,CAAA,EACF,kBAAC,QAAD;WAAM,WAAU;qBAA4B;WAA8B,CAAA,CACpE;;SACR,kBAAC,SAAD;UAAO,WAAU;oBAAjB,CACE,kBAAC,SAAD;WACE,MAAK;WACL,cAAW;WACX,SAAS,EAAK;WACd,WAAW,MACT,EAAQ;YAAE,GAAG;YAAM,2BAA2B,EAAE,OAAO;YAAS,CAAC;WAEnE,CAAA,EACF,kBAAC,QAAD;WAAM,WAAU;qBAA4B;WAErC,CAAA,CACD;;SACJ;;OACN,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,UAAD;SACE,MAAK;SACL,eAAe;AAGb,UAFA,EAAY,GAAM,EAClB,EAAa,KAAK,EAClB,EAAQ,EAAU;;SAEpB,WAAU;mBACX;SAEQ,CAAA,EACT,kBAAC,UAAD;SACE,MAAK;SACL,eAAe,KAAK,GAAQ;SAC5B,WAAU;mBAET,IAAY,iBAAiB;SACvB,CAAA,CACL;;OACI;;KAGb,KAAW,EAAO,WAAW,IAC5B,kBAAC,UAAD;MAAQ,WAAU;gBAA8C;MAAwB,CAAA,GACtF,EAAO,WAAW,IACpB,kBAAC,GAAD;MACE,cAAa;MACb,OAAM;MACN,aAAY;MACZ,QACE,kBAAC,UAAD;OACE,MAAK;OACL,SAAS;OACT,WAAU;iBAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,UAAW,CAAA,EAAA,eAEpB;;MAEX,CAAA,GAEF,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,SAAD;OAAO,WAAU;iBAAjB,CACE,kBAAC,SAAD;QAAO,WAAU;kBACf,kBAAC,MAAD,EAAA,UAAA;SACE,kBAAC,MAAD;UAAI,WAAU;oBAAY;UAAS,CAAA;SACnC,kBAAC,MAAD;UAAI,WAAU;oBAAY;UAAS,CAAA;SACnC,kBAAC,MAAD;UAAI,WAAU;oBAAY;UAAS,CAAA;SACnC,kBAAC,MAAD;UAAI,WAAU;oBAAY;UAAW,CAAA;SACrC,kBAAC,MAAD;UAAI,WAAU;oBAAY;UAAa,CAAA;SACvC,kBAAC,MAAD;UAAI,WAAU;oBAAY;UAAa,CAAA;SACvC,kBAAC,MAAD;UAAI,WAAU;oBAAuB;UAAY,CAAA;SAC9C,EAAA,CAAA;QACC,CAAA,EACR,kBAAC,SAAD;QAAO,WAAU;kBACd,EAAO,KAAK,MACX,kBAAC,MAAD;SAAe,WAAU;mBAAzB;UACE,kBAAC,MAAD;WAAI,WAAU;qBAA+B,EAAE;WAAU,CAAA;UACzD,kBAAC,MAAD;WAAI,WAAU;qBAAa,EAAE;WAAU,CAAA;UACvC,kBAAC,MAAD;WAAI,WAAU;qBACZ,kBAAC,QAAD;YAAM,WAAU;sBACb,EAAE;YACE,CAAA;WACJ,CAAA;UACL,kBAAC,MAAD;WAAI,WAAU;qBAAqB,EAAE,WAAW,QAAQ;WAAU,CAAA;UAClE,kBAAC,MAAD;WAAI,WAAU;qBACX,EAAE,mBAAmB,aAAa;WAChC,CAAA;UACL,kBAAC,MAAD;WAAI,WAAU;qBAAqB,EAAE;WAAc,CAAA;UACnD,kBAAC,MAAD;WAAI,WAAU;qBAAd,CACE,kBAAC,UAAD;YACE,MAAK;YACL,eAAe,EAAS,EAAE;YAC1B,WAAU;sBAHZ,CAKE,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA,EAAA,OAEtB;eACR,EAAE,OAAO,0BACR,kBAAC,UAAD;YACE,MAAK;YACL,eAAe,EAAmB,EAAE,GAAG;YACvC,WAAU;sBAHZ,CAKE,kBAAC,GAAD,EAAQ,WAAU,UAAW,CAAA,EAAA,SAEtB;cAER;;UACF;WAjCI,EAAE,GAiCN,CACL;QACI,CAAA,CACF;;MACJ,CAAA;KAEH;;GAEP,kBAAC,GAAD;IACE,MAAM,MAAoB;IAC1B,eAAe,MAAS,CAAC,KAAQ,EAAmB,KAAK;cAEzD,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD,EAAA,UAAkB,sBAAqC,CAAA,EACvD,kBAAC,GAAD,EAAA,UAAwB,0BAA+C,CAAA,CACrD,EAAA,CAAA,EACpB,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;KAAmB,eAAe,EAAmB,KAAK;eAAE;KAA0B,CAAA,EACtF,kBAAC,GAAD;KACE,WAAU;KACV,eAAe;AAEb,MADI,KAAsB,EAAO,EAAgB,EACjD,EAAmB,KAAK;;eAE3B;KAEmB,CAAA,CACF,EAAA,CAAA,CACD,EAAA,CAAA;IACT,CAAA;GACV"}
@@ -6,7 +6,7 @@ import { useCallback as i, useEffect as a, useState as o } from "react";
6
6
  import { ArrowLeft as ee, Check as te, ChevronDown as s, ChevronUp as c, Clock as l, Download as u, ExternalLink as d, FileJson as f, FlaskConical as ne, GitBranch as p, Hash as re, PackageCheck as ie, RefreshCw as m, ShieldCheck as ae, Tag as oe, User as se, X as h } from "lucide-react";
7
7
  import { graphqlFetch as g } from "@burdenoff/fe-libs/shared/graphql";
8
8
  import { jsx as _, jsxs as v } from "react/jsx-runtime";
9
- import { useNavigate as ce } from "react-router-dom";
9
+ import { useNavigate as ce } from "react-router";
10
10
  import { IllustratedEmptyState as le, PagePurpose as ue } from "@burdenoff/fe-libs/ui";
11
11
  //#region src/pages/AdminSubmissionsQueuePage.tsx
12
12
  var de = "\n query PendingReviewProducts($limit: Int, $offset: Int) {\n pendingReviewProducts(limit: $limit, offset: $offset) {\n totalCount\n products {\n id\n slug\n name\n type\n nature\n status\n pricingModel\n price\n currency\n description\n submittedAt\n submittedBy\n category\n icon\n productId\n zipFileUrl\n manifest\n requiredPermissions\n publisher {\n id\n name\n email\n }\n }\n }\n }\n", fe = "\n query AdminProducts($limit: Int, $offset: Int, $status: ProductStatus) {\n products(filter: { status: $status }, limit: $limit, offset: $offset) {\n id\n slug\n name\n type\n nature\n status\n pricingModel\n price\n currency\n description\n submittedAt\n submittedBy\n category\n icon\n productId\n zipFileUrl\n manifest\n requiredPermissions\n publisher {\n id\n name\n email\n }\n }\n }\n", pe = "\n mutation ApproveProduct($id: ID!, $notes: String) {\n approveProduct(id: $id, notes: $notes) {\n id\n status\n publishedAt\n }\n }\n", me = "\n mutation RejectProduct($id: ID!, $reason: String!) {\n rejectProduct(id: $id, reason: $reason) {\n id\n status\n rejectedReason\n }\n }\n", he = "\n mutation DelistProduct($id: ID!, $reason: String) {\n delistProduct(id: $id, reason: $reason) {\n id\n status\n }\n }\n", ge = "\n query AdminAppPackageUrl($id: ID!) {\n application(id: $id) {\n id\n versions(first: 1) {\n edges {\n node {\n id\n packageUrl\n }\n }\n }\n }\n }\n", _e = "\n mutation RefreshApplicationVersionPackageUrl($id: ID!) {\n refreshApplicationVersionPackageUrl(id: $id)\n }\n", ve = "\n query AdminPendingVersionApps($limit: Int, $offset: Int) {\n applicationsWithPendingVersions(limit: $limit, offset: $offset) {\n totalCount\n edges {\n node {\n id\n name\n slug\n iconUrl\n shortDescription\n description\n versions(first: 5) {\n edges {\n node {\n id\n version\n status\n releaseNotes\n packageUrl\n packageSize\n isLatest\n reviewNotes\n manifest\n createdAt\n }\n }\n }\n }\n }\n }\n }\n", ye = "\n mutation AdminApproveApplicationVersion($id: ID!, $notes: String) {\n approveApplicationVersion(id: $id, notes: $notes) {\n id\n status\n }\n }\n", be = "\n mutation AdminRejectApplicationVersion($id: ID!, $reason: String!) {\n rejectApplicationVersion(id: $id, reason: $reason) {\n id\n status\n }\n }\n", xe = [