@fayz-ai/plugin-reputation 0.9.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +30 -5
  2. package/dist/chunk-BGCEG4NA.js +410 -0
  3. package/dist/chunk-BGCEG4NA.js.map +1 -0
  4. package/dist/components/ProductRating.d.ts +15 -0
  5. package/dist/components/ProductRating.d.ts.map +1 -0
  6. package/dist/components/ProductReviews.d.ts +12 -0
  7. package/dist/components/ProductReviews.d.ts.map +1 -0
  8. package/dist/components/Stars.d.ts +15 -0
  9. package/dist/components/Stars.d.ts.map +1 -0
  10. package/dist/components/TopReviews.d.ts +13 -0
  11. package/dist/components/TopReviews.d.ts.map +1 -0
  12. package/dist/data/index.d.ts +4 -1
  13. package/dist/data/index.d.ts.map +1 -1
  14. package/dist/data/mock.d.ts +2 -0
  15. package/dist/data/mock.d.ts.map +1 -1
  16. package/dist/data/registries.d.ts +3 -0
  17. package/dist/data/registries.d.ts.map +1 -0
  18. package/dist/data/supabase.d.ts +5 -1
  19. package/dist/data/supabase.d.ts.map +1 -1
  20. package/dist/data/tables.d.ts +8 -0
  21. package/dist/data/tables.d.ts.map +1 -0
  22. package/dist/data/types.d.ts +24 -3
  23. package/dist/data/types.d.ts.map +1 -1
  24. package/dist/hooks/index.d.ts +4 -0
  25. package/dist/hooks/index.d.ts.map +1 -1
  26. package/dist/hooks/useManagedReviews.d.ts +23 -0
  27. package/dist/hooks/useManagedReviews.d.ts.map +1 -0
  28. package/dist/hooks/useProductReviews.d.ts +23 -0
  29. package/dist/hooks/useProductReviews.d.ts.map +1 -0
  30. package/dist/index.d.ts +9 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +238 -48
  33. package/dist/index.js.map +1 -1
  34. package/dist/migrations/index.d.ts +6 -0
  35. package/dist/migrations/index.d.ts.map +1 -0
  36. package/dist/public/index.d.ts +14 -2
  37. package/dist/public/index.d.ts.map +1 -1
  38. package/dist/public/index.js +295 -84
  39. package/dist/public/index.js.map +1 -1
  40. package/dist/types.d.ts +57 -0
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/views/ReputationHome.d.ts.map +1 -1
  43. package/package.json +6 -5
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/views/ReputationHome.tsx","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import React from 'react'\nimport { Star, Send, MessageSquare } from 'lucide-react'\nimport { Button, PageHeaderActions } from '@fayz-ai/ui'\n\ninterface Review {\n id: string\n author: string\n source: 'Google' | 'Facebook'\n rating: number\n text: string\n date: string\n replied: boolean\n}\n\nconst REVIEWS: Review[] = [\n { id: '1', author: 'Camila R.', source: 'Google', rating: 5, text: 'Absolutely loved the experience. The team is professional and friendly!', date: 'Jun 15', replied: true },\n { id: '2', author: 'Tom B.', source: 'Facebook', rating: 5, text: 'Booking was effortless and the results were fantastic.', date: 'Jun 13', replied: false },\n { id: '3', author: 'Aisha K.', source: 'Google', rating: 4, text: 'Great service overall, slightly long wait but worth it.', date: 'Jun 11', replied: false },\n { id: '4', author: 'Marco P.', source: 'Google', rating: 5, text: 'Best in town. Highly recommend to anyone.', date: 'Jun 9', replied: true },\n { id: '5', author: 'Lena S.', source: 'Facebook', rating: 3, text: 'Good but the pricing could be clearer up front.', date: 'Jun 6', replied: false },\n]\n\nconst DISTRIBUTION = [\n { stars: 5, count: 182 },\n { stars: 4, count: 41 },\n { stars: 3, count: 12 },\n { stars: 2, count: 4 },\n { stars: 1, count: 3 },\n]\n\nfunction Stars({ rating, className }: { rating: number; className?: string }) {\n return (\n <div className={`flex items-center gap-0.5 ${className ?? ''}`}>\n {[1, 2, 3, 4, 5].map((i) => (\n <Star key={i} className={`h-4 w-4 ${i <= rating ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/30'}`} />\n ))}\n </div>\n )\n}\n\nexport function ReputationHome() {\n const total = DISTRIBUTION.reduce((s, d) => s + d.count, 0)\n return (\n <div className=\"mx-auto max-w-6xl p-6\">\n <PageHeaderActions>\n <Button><Send className=\"mr-1.5 h-4 w-4\" /> Request reviews</Button>\n </PageHeaderActions>\n\n <div className=\"mt-6 grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <div className=\"rounded-card border border-border bg-card p-5 text-center\">\n <p className=\"text-4xl font-bold text-foreground\">4.8</p>\n <Stars rating={5} className=\"mt-1 justify-center\" />\n <p className=\"mt-1 text-xs text-muted-foreground\">{total} reviews</p>\n </div>\n <div className=\"rounded-card border border-border bg-card p-5 lg:col-span-2\">\n <div className=\"space-y-1.5\">\n {DISTRIBUTION.map((d) => (\n <div key={d.stars} className=\"flex items-center gap-2 text-xs\">\n <span className=\"w-6 text-muted-foreground\">{d.stars}★</span>\n <div className=\"h-2 flex-1 overflow-hidden rounded-full bg-muted\">\n <div className=\"h-full rounded-full bg-amber-400\" style={{ width: `${(d.count / total) * 100}%` }} />\n </div>\n <span className=\"w-8 text-right text-muted-foreground\">{d.count}</span>\n </div>\n ))}\n </div>\n </div>\n </div>\n\n <div className=\"mt-6 space-y-3\">\n {REVIEWS.map((r) => (\n <div key={r.id} className=\"rounded-card border border-border bg-card p-4\">\n <div className=\"flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-9 w-9 items-center justify-center rounded-full bg-muted text-sm font-semibold text-foreground\">\n {r.author[0]}\n </div>\n <div>\n <p className=\"text-sm font-semibold text-foreground\">{r.author}</p>\n <p className=\"text-xs text-muted-foreground\">{r.source} · {r.date}</p>\n </div>\n </div>\n <Stars rating={r.rating} />\n </div>\n <p className=\"mt-2 text-sm text-muted-foreground\">{r.text}</p>\n <div className=\"mt-2\">\n {r.replied ? (\n <span className=\"text-xs font-medium text-emerald-600\">✓ Replied</span>\n ) : (\n <Button variant=\"outline\" size=\"sm\"><MessageSquare className=\"mr-1.5 h-3.5 w-3.5\" /> Reply</Button>\n )}\n </div>\n </div>\n ))}\n </div>\n\n <p className=\"mt-4 text-center text-xs text-muted-foreground\">\n Mock preview · Google/Facebook review sync + automated requests ship in a later milestone.\n </p>\n </div>\n )\n}\n","import React from 'react'\nimport type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'\nimport { ReputationHome } from './views/ReputationHome'\n\n// ---------------------------------------------------------------------------\n// @fayz-ai/plugin-reputation — reviews & reputation (GoHighLevel \"Reputation\").\n// Universal plugin. M1 ships a rich mock home; Google/Facebook review sync +\n// automated review requests (over connectors + automations) come later.\n//\n// The public/website surface (createReputationWebsite + hooks + ReviewsList)\n// lives in the lean './public' subpath so a marketing-site host imports it\n// WITHOUT pulling the admin view's @fayz-ai/ui graph.\n// ---------------------------------------------------------------------------\n\nexport interface ReputationPluginOptions {\n navPosition?: number\n navSection?: 'main' | 'secondary' | 'settings'\n navLabel?: string\n scope?: PluginScope\n verticalId?: VerticalId\n}\n\nexport function createReputationPlugin(options?: ReputationPluginOptions): PluginManifest {\n const PageComponent: React.ComponentType<unknown> = () => React.createElement(ReputationHome)\n PageComponent.displayName = 'ReputationHome'\n\n return {\n id: 'reputation',\n name: options?.navLabel ?? 'Reputation',\n icon: 'Award',\n version: '1.0.0',\n scope: options?.scope ?? 'universal',\n verticalId: options?.verticalId,\n defaultEnabled: true,\n dependencies: [],\n declaredFeatures: [{ id: 'reputation', label: 'Reputation', group: 'Retain' }],\n navigation: [\n {\n section: options?.navSection ?? 'main',\n position: options?.navPosition ?? 8,\n label: options?.navLabel ?? 'Reputation',\n route: '/reputation',\n icon: 'Award',\n permission: { feature: 'reputation', action: 'read' as const },\n },\n ],\n routes: [\n {\n path: '/reputation',\n component: PageComponent,\n permission: { feature: 'reputation', action: 'read' as const },\n },\n ],\n widgets: [],\n // How the review request reaches the customer — the two the Fayz plugin\n // page lists for Reputação.\n integrations: ['whatsapp', 'twilio'],\n }\n}\n"]}
1
+ {"version":3,"sources":["../src/views/ReputationHome.tsx","../src/migrations/index.ts","../src/index.ts"],"names":["React"],"mappings":"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","file":"index.js","sourcesContent":["import React from 'react'\nimport { Star, Send, Check, X, Trash2 } from 'lucide-react'\nimport { Button, PageHeaderActions, DataTable } from '@fayz-ai/ui'\nimport { useReviewSummary } from '../hooks/useReviewSummary'\nimport { useManagedReviews } from '../hooks/useManagedReviews'\nimport type { Review, ReviewStatus } from '../types'\n\n// ---------------------------------------------------------------------------\n// The reputation backoffice.\n//\n// Was five hardcoded reviews and a hardcoded 4.8. It now reads whatever provider\n// the host installed — Supabase when there is a session, the seed otherwise —\n// and the feed is a moderation queue: only `approved` rows reach the storefront,\n// so this screen is the thing that decides what a shopper sees.\n// ---------------------------------------------------------------------------\n\nconst STATUS_TABS: Array<{ id: ReviewStatus | 'all'; label: string }> = [\n { id: 'pending', label: 'Pendentes' },\n { id: 'approved', label: 'Publicadas' },\n { id: 'rejected', label: 'Recusadas' },\n { id: 'all', label: 'Todas' },\n]\n\nfunction Stars({ rating, className }: { rating: number; className?: string }) {\n return (\n <div className={`flex items-center gap-0.5 ${className ?? ''}`}>\n {[1, 2, 3, 4, 5].map((i) => (\n <Star\n key={i}\n className={`h-4 w-4 ${i <= Math.round(rating) ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/30'}`}\n />\n ))}\n </div>\n )\n}\n\nexport function ReputationHome() {\n const [tab, setTab] = React.useState<ReviewStatus | 'all'>('pending')\n const { summary: loadedSummary } = useReviewSummary()\n // null until the first load resolves; an empty aggregate renders the same\n // \"—\" as a business with no reviews, which is the honest thing to show.\n const summary = loadedSummary ?? { average: 0, count: 0, distribution: [] }\n const { reviews, loading, canModerate, setStatus, remove } = useManagedReviews(\n tab === 'all' ? {} : { status: tab },\n )\n\n const distribution = summary.distribution ?? []\n const total = summary.count\n\n const columns = React.useMemo(\n () => [\n {\n id: 'author',\n header: 'Cliente',\n cell: ({ row }: { row: { original: Review } }) => (\n <div className=\"min-w-[9rem]\">\n <p className=\"font-medium text-foreground\">{row.original.author}</p>\n <p className=\"text-xs text-muted-foreground\">{row.original.date || '—'}</p>\n </div>\n ),\n },\n {\n id: 'rating',\n header: 'Nota',\n cell: ({ row }: { row: { original: Review } }) => <Stars rating={row.original.rating} />,\n },\n {\n id: 'product',\n header: 'Produto',\n cell: ({ row }: { row: { original: Review } }) => (\n <span className=\"text-sm text-muted-foreground\">\n {/* No product means it is about the business, not a gap in the data. */}\n {row.original.productName ?? <em className=\"opacity-70\">a loja</em>}\n </span>\n ),\n },\n {\n id: 'text',\n header: 'Avaliação',\n cell: ({ row }: { row: { original: Review } }) => (\n <div className=\"max-w-[28rem]\">\n {row.original.title && <p className=\"font-medium text-foreground\">{row.original.title}</p>}\n <p className=\"line-clamp-2 text-sm text-muted-foreground\">{row.original.text}</p>\n {row.original.replyText && (\n <p className=\"mt-1 line-clamp-1 border-l-2 border-border pl-2 text-xs text-muted-foreground\">\n Resposta: {row.original.replyText}\n </p>\n )}\n </div>\n ),\n },\n {\n id: 'actions',\n header: '',\n cell: ({ row }: { row: { original: Review } }) => {\n const r = row.original\n return (\n <div className=\"flex items-center justify-end gap-1\">\n {r.status !== 'approved' && (\n <Button size=\"sm\" variant=\"ghost\" title=\"Publicar\" onClick={() => void setStatus(r.id, 'approved')}>\n <Check className=\"h-4 w-4\" />\n </Button>\n )}\n {r.status !== 'rejected' && (\n <Button size=\"sm\" variant=\"ghost\" title=\"Recusar\" onClick={() => void setStatus(r.id, 'rejected')}>\n <X className=\"h-4 w-4\" />\n </Button>\n )}\n <Button size=\"sm\" variant=\"ghost\" title=\"Excluir\" onClick={() => void remove(r.id)}>\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n )\n },\n },\n ],\n [setStatus, remove],\n )\n\n return (\n <div className=\"mx-auto max-w-6xl p-6\">\n <PageHeaderActions>\n <Button>\n <Send className=\"mr-1.5 h-4 w-4\" /> Pedir avaliações\n </Button>\n </PageHeaderActions>\n\n <div className=\"mt-6 grid grid-cols-1 gap-4 lg:grid-cols-3\">\n <div className=\"rounded-card border border-border bg-card p-5 text-center\">\n <p className=\"text-4xl font-bold text-foreground\">\n {total === 0 ? '—' : summary.average.toFixed(1).replace('.', ',')}\n </p>\n <Stars rating={summary.average} className=\"mt-1 justify-center\" />\n <p className=\"mt-1 text-xs text-muted-foreground\">\n {total} {total === 1 ? 'avaliação' : 'avaliações'}\n </p>\n </div>\n <div className=\"rounded-card border border-border bg-card p-5 lg:col-span-2\">\n <div className=\"space-y-1.5\">\n {distribution.map((d) => (\n <div key={d.stars} className=\"flex items-center gap-2 text-xs\">\n <span className=\"w-6 text-muted-foreground\">{d.stars}★</span>\n <div className=\"h-2 flex-1 overflow-hidden rounded-full bg-muted\">\n <div\n className=\"h-full rounded-full bg-amber-400\"\n style={{ width: `${total === 0 ? 0 : (d.count / total) * 100}%` }}\n />\n </div>\n <span className=\"w-8 text-right text-muted-foreground\">{d.count}</span>\n </div>\n ))}\n </div>\n </div>\n </div>\n\n <div className=\"mt-6 flex flex-wrap gap-1\">\n {STATUS_TABS.map((t) => (\n <Button key={t.id} size=\"sm\" variant={tab === t.id ? 'default' : 'ghost'} onClick={() => setTab(t.id)}>\n {t.label}\n </Button>\n ))}\n </div>\n\n <div className=\"mt-3\">\n <DataTable\n columns={columns}\n data={reviews}\n loading={loading}\n emptyMessage={\n canModerate\n ? 'Nenhuma avaliação aqui ainda.'\n : 'Este app está lendo avaliações públicas e não pode moderá-las.'\n }\n />\n </div>\n </div>\n )\n}\n","// AUTO-GENERATED from 001_product_reviews.sql — regenerate with scripts/embed-migrations.mjs\n// SQL files are the source of truth; this inline copy lets the manifest declare\n// migrations as data. Do not edit by hand — run the embed script instead.\n\nexport const MIGRATION_001_PRODUCT_REVIEWS = `-- ============================================================================\n-- plugin-reputation 001: product reviews + public (anon) read surface\n--\n-- The plugin shipped with a mock-only reputation home. This is its first real\n-- table, and it carries BOTH shapes of review the plugin talks about:\n-- a business review — product_id IS NULL (the Google/Facebook feed)\n-- a product review — product_id IS NOT NULL (the storefront PDP)\n-- One table, because they are the same row to a merchant: an author, a rating,\n-- some words, and a reply. Prefix: plg_reputation_.\n--\n-- §1 plg_reputation_reviews — the reviews themselves\n-- §2 RLS: authenticated tenant CRUD (the backoffice)\n-- §3 v_public_product_reviews — anon-readable, APPROVED only, whitelisted\n-- §4 v_public_product_ratings — anon-readable per-product aggregate\n--\n-- Anon-access design mirrors plugin-blog's: the storefront runs with the\n-- publishable key and no session, so canonical tenant RLS yields nothing. Anon\n-- read is granted ONLY through the column-whitelisted, approved-only views; the\n-- front provider filters them by tenant_id. Idempotent + safe to re-run.\n--\n-- product_id deliberately has NO foreign key. plugin-shop owns plg_shop_products\n-- and may not be installed in a given pool — a FK here would make this migration\n-- fail on every non-commerce tenant. product_name/product_slug are a display\n-- snapshot taken at write time so the homepage card can render and link without\n-- this plugin ever reading another plugin's table.\n-- ============================================================================\n\n-- §1 — reviews\nCREATE TABLE IF NOT EXISTS public.plg_reputation_reviews (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n product_id uuid,\n product_name text,\n product_slug text,\n order_id uuid,\n author_name text NOT NULL,\n author_avatar_url text,\n rating numeric(2,1) NOT NULL CHECK (rating >= 1 AND rating <= 5),\n title text,\n body text NOT NULL DEFAULT '',\n source text NOT NULL DEFAULT 'Website',\n status text NOT NULL DEFAULT 'pending'\n CHECK (status IN ('pending', 'approved', 'rejected')),\n verified boolean NOT NULL DEFAULT false,\n reply_text text,\n replied_at timestamptz,\n published_at timestamptz,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_reputation_reviews ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_reputation_reviews_tenant ON public.plg_reputation_reviews(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_plg_reputation_reviews_product ON public.plg_reputation_reviews(tenant_id, product_id, status);\nCREATE INDEX IF NOT EXISTS idx_plg_reputation_reviews_recent ON public.plg_reputation_reviews(tenant_id, status, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_plg_reputation_reviews_order ON public.plg_reputation_reviews(order_id);\n\n-- §2 — RLS: authenticated tenant CRUD (the backoffice writes here)\nDROP POLICY IF EXISTS plg_reputation_reviews_select ON public.plg_reputation_reviews;\nDROP POLICY IF EXISTS plg_reputation_reviews_insert ON public.plg_reputation_reviews;\nDROP POLICY IF EXISTS plg_reputation_reviews_update ON public.plg_reputation_reviews;\nDROP POLICY IF EXISTS plg_reputation_reviews_delete ON public.plg_reputation_reviews;\nCREATE POLICY plg_reputation_reviews_select ON public.plg_reputation_reviews FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_reputation_reviews_insert ON public.plg_reputation_reviews FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_reputation_reviews_update ON public.plg_reputation_reviews FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_reputation_reviews_delete ON public.plg_reputation_reviews FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- §3 — anon-readable review feed (owner-rights view = deliberate, whitelisted,\n-- APPROVED only). author_avatar_url and the reply are safe to show; e-mail and\n-- order_id are not, so they are simply absent from the projection.\nCREATE OR REPLACE VIEW public.v_public_product_reviews AS\nSELECT\n r.id,\n r.tenant_id,\n r.product_id,\n r.product_name,\n r.product_slug,\n r.author_name,\n r.author_avatar_url,\n r.rating,\n r.title,\n r.body,\n r.source,\n r.verified,\n r.reply_text,\n r.replied_at,\n COALESCE(r.published_at, r.created_at) AS published_at\nFROM public.plg_reputation_reviews r\nWHERE r.status = 'approved';\n\nGRANT SELECT ON public.v_public_product_reviews TO anon, authenticated, service_role;\n\n-- §4 — per-product aggregate. Exists so a PDP costs ONE row instead of every\n-- review, and so a catalogue page can fetch ratings for a whole grid with a\n-- single \\`product_id=in.(…)\\`. Business reviews (product_id IS NULL) are excluded\n-- on purpose: they are not about any product.\nCREATE OR REPLACE VIEW public.v_public_product_ratings AS\nSELECT\n r.tenant_id,\n r.product_id,\n ROUND(AVG(r.rating), 1)::numeric(2,1) AS average,\n COUNT(*)::int AS count,\n COUNT(*) FILTER (WHERE ROUND(r.rating) = 5)::int AS star5,\n COUNT(*) FILTER (WHERE ROUND(r.rating) = 4)::int AS star4,\n COUNT(*) FILTER (WHERE ROUND(r.rating) = 3)::int AS star3,\n COUNT(*) FILTER (WHERE ROUND(r.rating) = 2)::int AS star2,\n COUNT(*) FILTER (WHERE ROUND(r.rating) = 1)::int AS star1\nFROM public.plg_reputation_reviews r\nWHERE r.status = 'approved' AND r.product_id IS NOT NULL\nGROUP BY r.tenant_id, r.product_id;\n\nGRANT SELECT ON public.v_public_product_ratings TO anon, authenticated, service_role;\n`\n\nexport const MIGRATIONS: Array<{ id: string; sql: string }> = [\n { id: \"001_product_reviews\", sql: MIGRATION_001_PRODUCT_REVIEWS },\n]\n","import React from 'react'\nimport type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'\nimport { createSafeDataProvider } from '@fayz-ai/core'\nimport { ReputationHome } from './views/ReputationHome'\nimport { ReputationProvider } from './context'\nimport { createMockReputationProvider, type ReputationSeed } from './data/mock'\nimport { createSupabaseReputationProvider } from './data/supabase'\nimport type { ReputationDataProvider } from './data/types'\nimport { reputationRegistries } from './data/registries'\nimport { MIGRATIONS } from './migrations'\n\n// ---------------------------------------------------------------------------\n// @fayz-ai/plugin-reputation — reviews & reputation (GoHighLevel \"Reputation\").\n// Universal plugin. M1 ships a rich mock home; Google/Facebook review sync +\n// automated review requests (over connectors + automations) come later.\n//\n// The public/website surface (createReputationWebsite + hooks + ReviewsList)\n// lives in the lean './public' subpath so a marketing-site host imports it\n// WITHOUT pulling the admin view's @fayz-ai/ui graph.\n// ---------------------------------------------------------------------------\n\n/** Version + prose for each migration, kept beside the manifest that ships it. */\nconst MIGRATION_META: Record<string, { version: string; description: string }> = {\n '001_product_reviews': {\n version: '1.0.0',\n description: 'Reviews table (business + product), RLS, and the anon-readable review/rating views',\n },\n}\n\nexport interface ReputationPluginOptions {\n navPosition?: number\n navSection?: 'main' | 'secondary' | 'settings'\n navLabel?: string\n scope?: PluginScope\n verticalId?: VerticalId\n /** Inject a custom provider. Overrides the safe Supabase/mock resolver. */\n dataProvider?: ReputationDataProvider\n /** Reviews to run on until the tables are populated (or in a demo). */\n seed?: ReputationSeed\n /** Tenant to read. Defaults to the active session tenant, which is what the\n * backoffice normally wants — pass this only for a fixed single-tenant app. */\n tenantId?: string\n}\n\nexport function createReputationPlugin(options?: ReputationPluginOptions): PluginManifest {\n // The screen used to render five hardcoded reviews. It now reads a provider,\n // so the plugin has to supply one: Supabase when a client and tenant exist,\n // the seed otherwise. createSafeDataProvider makes that choice lazily, per\n // call, which is what lets a host boot before its session is resolved.\n const provider =\n options?.dataProvider ??\n createSafeDataProvider(\n () => createSupabaseReputationProvider({ tenantId: options?.tenantId }),\n () => createMockReputationProvider({ seed: options?.seed }),\n )\n\n const PageComponent: React.ComponentType<unknown> = () =>\n React.createElement(ReputationProvider, {\n value: { provider },\n children: React.createElement(ReputationHome),\n })\n PageComponent.displayName = 'ReputationHome'\n\n return {\n id: 'reputation',\n name: options?.navLabel ?? 'Reputation',\n icon: 'Award',\n version: '1.0.0',\n scope: options?.scope ?? 'universal',\n verticalId: options?.verticalId,\n defaultEnabled: true,\n dependencies: [],\n declaredFeatures: [{ id: 'reputation', label: 'Reputation', group: 'Retain' }],\n navigation: [\n {\n section: options?.navSection ?? 'main',\n position: options?.navPosition ?? 8,\n label: options?.navLabel ?? 'Reputation',\n route: '/reputation',\n icon: 'Award',\n permission: { feature: 'reputation', action: 'read' as const },\n },\n ],\n routes: [\n {\n path: '/reputation',\n component: PageComponent,\n permission: { feature: 'reputation', action: 'read' as const },\n },\n ],\n widgets: [],\n // How the review request reaches the customer — the two the Fayz plugin\n // page lists for Reputação.\n integrations: ['whatsapp', 'twilio'],\n // Owns the reviews table and the two anon-read views. The backoffice here\n // writes them; the storefront surface in './public' only reads. Declared on\n // this manifest and not on the website one so a host that installs both\n // does not register the same migration twice.\n migrations: [\n ...MIGRATIONS.map((m) => ({\n id: `reputation-${m.id}`,\n version: MIGRATION_META[m.id]?.version ?? '1.0.0',\n sql: m.sql,\n description: MIGRATION_META[m.id]?.description ?? m.id,\n })),\n ],\n }\n}\n"]}
@@ -0,0 +1,6 @@
1
+ export declare const MIGRATION_001_PRODUCT_REVIEWS = "-- ============================================================================\n-- plugin-reputation 001: product reviews + public (anon) read surface\n--\n-- The plugin shipped with a mock-only reputation home. This is its first real\n-- table, and it carries BOTH shapes of review the plugin talks about:\n-- a business review \u2014 product_id IS NULL (the Google/Facebook feed)\n-- a product review \u2014 product_id IS NOT NULL (the storefront PDP)\n-- One table, because they are the same row to a merchant: an author, a rating,\n-- some words, and a reply. Prefix: plg_reputation_.\n--\n-- \u00A71 plg_reputation_reviews \u2014 the reviews themselves\n-- \u00A72 RLS: authenticated tenant CRUD (the backoffice)\n-- \u00A73 v_public_product_reviews \u2014 anon-readable, APPROVED only, whitelisted\n-- \u00A74 v_public_product_ratings \u2014 anon-readable per-product aggregate\n--\n-- Anon-access design mirrors plugin-blog's: the storefront runs with the\n-- publishable key and no session, so canonical tenant RLS yields nothing. Anon\n-- read is granted ONLY through the column-whitelisted, approved-only views; the\n-- front provider filters them by tenant_id. Idempotent + safe to re-run.\n--\n-- product_id deliberately has NO foreign key. plugin-shop owns plg_shop_products\n-- and may not be installed in a given pool \u2014 a FK here would make this migration\n-- fail on every non-commerce tenant. product_name/product_slug are a display\n-- snapshot taken at write time so the homepage card can render and link without\n-- this plugin ever reading another plugin's table.\n-- ============================================================================\n\n-- \u00A71 \u2014 reviews\nCREATE TABLE IF NOT EXISTS public.plg_reputation_reviews (\n id uuid PRIMARY KEY DEFAULT gen_random_uuid(),\n tenant_id uuid NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE,\n product_id uuid,\n product_name text,\n product_slug text,\n order_id uuid,\n author_name text NOT NULL,\n author_avatar_url text,\n rating numeric(2,1) NOT NULL CHECK (rating >= 1 AND rating <= 5),\n title text,\n body text NOT NULL DEFAULT '',\n source text NOT NULL DEFAULT 'Website',\n status text NOT NULL DEFAULT 'pending'\n CHECK (status IN ('pending', 'approved', 'rejected')),\n verified boolean NOT NULL DEFAULT false,\n reply_text text,\n replied_at timestamptz,\n published_at timestamptz,\n created_at timestamptz NOT NULL DEFAULT now(),\n updated_at timestamptz NOT NULL DEFAULT now()\n);\nALTER TABLE public.plg_reputation_reviews ENABLE ROW LEVEL SECURITY;\nCREATE INDEX IF NOT EXISTS idx_plg_reputation_reviews_tenant ON public.plg_reputation_reviews(tenant_id);\nCREATE INDEX IF NOT EXISTS idx_plg_reputation_reviews_product ON public.plg_reputation_reviews(tenant_id, product_id, status);\nCREATE INDEX IF NOT EXISTS idx_plg_reputation_reviews_recent ON public.plg_reputation_reviews(tenant_id, status, created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_plg_reputation_reviews_order ON public.plg_reputation_reviews(order_id);\n\n-- \u00A72 \u2014 RLS: authenticated tenant CRUD (the backoffice writes here)\nDROP POLICY IF EXISTS plg_reputation_reviews_select ON public.plg_reputation_reviews;\nDROP POLICY IF EXISTS plg_reputation_reviews_insert ON public.plg_reputation_reviews;\nDROP POLICY IF EXISTS plg_reputation_reviews_update ON public.plg_reputation_reviews;\nDROP POLICY IF EXISTS plg_reputation_reviews_delete ON public.plg_reputation_reviews;\nCREATE POLICY plg_reputation_reviews_select ON public.plg_reputation_reviews FOR SELECT TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_reputation_reviews_insert ON public.plg_reputation_reviews FOR INSERT TO authenticated WITH CHECK (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_reputation_reviews_update ON public.plg_reputation_reviews FOR UPDATE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\nCREATE POLICY plg_reputation_reviews_delete ON public.plg_reputation_reviews FOR DELETE TO authenticated USING (tenant_id IN (SELECT public.user_tenant_ids()));\n\n-- \u00A73 \u2014 anon-readable review feed (owner-rights view = deliberate, whitelisted,\n-- APPROVED only). author_avatar_url and the reply are safe to show; e-mail and\n-- order_id are not, so they are simply absent from the projection.\nCREATE OR REPLACE VIEW public.v_public_product_reviews AS\nSELECT\n r.id,\n r.tenant_id,\n r.product_id,\n r.product_name,\n r.product_slug,\n r.author_name,\n r.author_avatar_url,\n r.rating,\n r.title,\n r.body,\n r.source,\n r.verified,\n r.reply_text,\n r.replied_at,\n COALESCE(r.published_at, r.created_at) AS published_at\nFROM public.plg_reputation_reviews r\nWHERE r.status = 'approved';\n\nGRANT SELECT ON public.v_public_product_reviews TO anon, authenticated, service_role;\n\n-- \u00A74 \u2014 per-product aggregate. Exists so a PDP costs ONE row instead of every\n-- review, and so a catalogue page can fetch ratings for a whole grid with a\n-- single `product_id=in.(\u2026)`. Business reviews (product_id IS NULL) are excluded\n-- on purpose: they are not about any product.\nCREATE OR REPLACE VIEW public.v_public_product_ratings AS\nSELECT\n r.tenant_id,\n r.product_id,\n ROUND(AVG(r.rating), 1)::numeric(2,1) AS average,\n COUNT(*)::int AS count,\n COUNT(*) FILTER (WHERE ROUND(r.rating) = 5)::int AS star5,\n COUNT(*) FILTER (WHERE ROUND(r.rating) = 4)::int AS star4,\n COUNT(*) FILTER (WHERE ROUND(r.rating) = 3)::int AS star3,\n COUNT(*) FILTER (WHERE ROUND(r.rating) = 2)::int AS star2,\n COUNT(*) FILTER (WHERE ROUND(r.rating) = 1)::int AS star1\nFROM public.plg_reputation_reviews r\nWHERE r.status = 'approved' AND r.product_id IS NOT NULL\nGROUP BY r.tenant_id, r.product_id;\n\nGRANT SELECT ON public.v_public_product_ratings TO anon, authenticated, service_role;\n";
2
+ export declare const MIGRATIONS: Array<{
3
+ id: string;
4
+ sql: string;
5
+ }>;
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,6BAA6B,y6LA+GzC,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAEzD,CAAA"}
@@ -9,6 +9,9 @@ export interface ReputationWebsiteOptions {
9
9
  seed?: ReputationSeed;
10
10
  /** Inject a custom provider. Overrides the safe mock/Supabase resolver. */
11
11
  dataProvider?: ReputationDataProvider;
12
+ /** Tenant whose approved reviews to read. An anon storefront has no session,
13
+ * so without this the Supabase provider has no tenant and falls back to seed. */
14
+ tenantId?: string;
12
15
  /** Heading for the optional /reviews screen. */
13
16
  heading?: string;
14
17
  scope?: PluginScope;
@@ -22,12 +25,21 @@ export interface ReputationWebsitePlugin {
22
25
  dataProvider: ReputationDataProvider;
23
26
  }
24
27
  export declare function createReputationWebsite(options?: ReputationWebsiteOptions): ReputationWebsitePlugin;
25
- export type { Review, ReviewSummary, ReviewSource, ReviewListQuery } from '../types';
28
+ export type { Review, ReviewSummary, ReviewSource, ReviewListQuery, ReviewStatus, ProductReviewQuery, TopReviewsQuery, } from '../types';
26
29
  export type { ReputationDataProvider } from '../data/types';
27
30
  export { createMockReputationProvider, createSupabaseReputationProvider } from '../data';
28
31
  export type { ReputationSeed } from '../data/mock';
29
32
  export { ReputationProvider, useReputationContext } from '../context';
30
33
  export type { ReputationContextValue } from '../context';
31
- export { useReviews, useReviewSummary } from '../hooks';
34
+ export { useReviews, useReviewSummary, useProductRating, useProductReviews, useTopReviews } from '../hooks';
35
+ export type { UseProductRatingResult, UseProductReviewsResult } from '../hooks';
32
36
  export { ReviewsList } from '../components/ReviewsList';
37
+ export { Stars } from '../components/Stars';
38
+ export type { StarsProps } from '../components/Stars';
39
+ export { ProductRating } from '../components/ProductRating';
40
+ export type { ProductRatingProps } from '../components/ProductRating';
41
+ export { ProductReviews } from '../components/ProductReviews';
42
+ export type { ProductReviewsProps } from '../components/ProductReviews';
43
+ export { TopReviews } from '../components/TopReviews';
44
+ export type { TopReviewsProps } from '../components/TopReviews';
33
45
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/public/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE5E,OAAO,EAAgC,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAIhF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAW3D,MAAM,WAAW,wBAAwB;IACvC,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yFAAyF;IACzF,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,sBAAsB,CAAA;IACrC,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,cAAc,CAAA;IACxB,QAAQ,EAAE,EAAE,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IACrC,YAAY,EAAE,sBAAsB,CAAA;CACrC;AAED,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,uBAAuB,CAkCnG;AAGD,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AACpF,YAAY,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAC3D,OAAO,EAAE,4BAA4B,EAAE,gCAAgC,EAAE,MAAM,SAAS,CAAA;AACxF,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AACrE,YAAY,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAA;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/public/index.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,EAAE,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAE5E,OAAO,EAAgC,KAAK,cAAc,EAAE,MAAM,cAAc,CAAA;AAIhF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAW3D,MAAM,WAAW,wBAAwB;IACvC,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,yFAAyF;IACzF,IAAI,CAAC,EAAE,cAAc,CAAA;IACrB,2EAA2E;IAC3E,YAAY,CAAC,EAAE,sBAAsB,CAAA;IACrC;sFACkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,gDAAgD;IAChD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,EAAE,cAAc,CAAA;IACxB,QAAQ,EAAE,EAAE,CAAC;QAAE,QAAQ,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IACrC,YAAY,EAAE,sBAAsB,CAAA;CACrC;AAED,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,uBAAuB,CAkCnG;AAGD,YAAY,EACV,MAAM,EACN,aAAa,EACb,YAAY,EACZ,eAAe,EACf,YAAY,EACZ,kBAAkB,EAClB,eAAe,GAChB,MAAM,UAAU,CAAA;AACjB,YAAY,EAAE,sBAAsB,EAAE,MAAM,eAAe,CAAA;AAC3D,OAAO,EAAE,4BAA4B,EAAE,gCAAgC,EAAE,MAAM,SAAS,CAAA;AACxF,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAA;AACrE,YAAY,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AAC3G,YAAY,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAA;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAC3C,YAAY,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAA;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AAC3D,YAAY,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAA;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAC7D,YAAY,EAAE,mBAAmB,EAAE,MAAM,8BAA8B,CAAA;AACvE,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACrD,YAAY,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA"}
@@ -1,64 +1,10 @@
1
- import { createContext, useContext, useState, useEffect, createElement } from 'react';
1
+ import { useReputationContext, useReviewSummary, createSupabaseReputationProvider, createMockReputationProvider, ReputationProvider } from '../chunk-BGCEG4NA.js';
2
+ export { ReputationProvider, createMockReputationProvider, createSupabaseReputationProvider, useReputationContext, useReviewSummary } from '../chunk-BGCEG4NA.js';
3
+ import { useState, useEffect, createElement } from 'react';
2
4
  import { createSafeDataProvider } from '@fayz-ai/core';
3
- import { jsx, jsxs } from 'react/jsx-runtime';
4
5
  import { Star } from 'lucide-react';
6
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
7
 
6
- // src/public/index.tsx
7
-
8
- // src/data/mock.ts
9
- var FALLBACK_REVIEWS = [
10
- { id: "r1", author: "Camila R.", source: "Google", rating: 5, text: "Excelente atendimento, recomendo!", date: "Jun 2025" },
11
- { id: "r2", author: "Tom B.", source: "Facebook", rating: 5, text: "Processo simples e resultado \xF3timo.", date: "Jun 2025" },
12
- { id: "r3", author: "Aisha K.", source: "Google", rating: 4, text: "Muito bom no geral.", date: "Jun 2025" }
13
- ];
14
- function computeSummary(reviews) {
15
- const count = reviews.length;
16
- const average = count === 0 ? 0 : Math.round(reviews.reduce((s, r) => s + r.rating, 0) / count * 10) / 10;
17
- const distribution = [5, 4, 3, 2, 1].map((stars) => ({
18
- stars,
19
- count: reviews.filter((r) => Math.round(r.rating) === stars).length
20
- }));
21
- return { average, count, distribution };
22
- }
23
- function createMockReputationProvider(options) {
24
- const reviews = options?.seed?.reviews ?? FALLBACK_REVIEWS;
25
- const summary = options?.seed?.summary ?? computeSummary(reviews);
26
- return {
27
- async listReviews(query) {
28
- let result = reviews;
29
- if (query?.minRating != null) result = result.filter((r) => r.rating >= query.minRating);
30
- if (query?.limit != null) result = result.slice(0, query.limit);
31
- return result;
32
- },
33
- async getSummary() {
34
- return summary;
35
- }
36
- };
37
- }
38
-
39
- // src/data/supabase.ts
40
- function createSupabaseReputationProvider() {
41
- const notImplemented = () => {
42
- throw new Error(
43
- "[plugin-reputation] Supabase provider not implemented yet \u2014 deferred to Phase 2. Run on the mock/seed provider (no Supabase client configured) for now."
44
- );
45
- };
46
- return {
47
- listReviews: (_query) => notImplemented(),
48
- getSummary: () => notImplemented()
49
- };
50
- }
51
- var ReputationContext = createContext(null);
52
- function ReputationProvider({ value, children }) {
53
- return /* @__PURE__ */ jsx(ReputationContext.Provider, { value, children });
54
- }
55
- function useReputationContext() {
56
- const ctx = useContext(ReputationContext);
57
- if (!ctx) {
58
- throw new Error("[plugin-reputation] useReputationContext must be used within <ReputationProvider>.");
59
- }
60
- return ctx;
61
- }
62
8
  function useReviews(query) {
63
9
  const { provider } = useReputationContext();
64
10
  const [reviews, setReviews] = useState([]);
@@ -85,30 +31,6 @@ function useReviews(query) {
85
31
  }, [provider, limit, minRating]);
86
32
  return { reviews, loading, error };
87
33
  }
88
- function useReviewSummary() {
89
- const { provider } = useReputationContext();
90
- const [summary, setSummary] = useState(null);
91
- const [loading, setLoading] = useState(true);
92
- const [error, setError] = useState(null);
93
- useEffect(() => {
94
- let active = true;
95
- setLoading(true);
96
- provider.getSummary().then((result) => {
97
- if (!active) return;
98
- setSummary(result);
99
- setError(null);
100
- }).catch((err) => {
101
- if (!active) return;
102
- setError(err instanceof Error ? err : new Error(String(err)));
103
- }).finally(() => {
104
- if (active) setLoading(false);
105
- });
106
- return () => {
107
- active = false;
108
- };
109
- }, [provider]);
110
- return { summary, loading, error };
111
- }
112
34
  function Stars({ rating }) {
113
35
  return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-0.5", children: [1, 2, 3, 4, 5].map((i) => /* @__PURE__ */ jsx(
114
36
  Star,
@@ -151,13 +73,302 @@ function ReviewsList({ limit, heading }) {
151
73
  ] }, review.id)) })
152
74
  ] }) });
153
75
  }
76
+ var EMPTY_SUMMARY = { average: 0, count: 0, distribution: [] };
77
+ function useProductRating(productId) {
78
+ const { provider } = useReputationContext();
79
+ const [rating, setRating] = useState(EMPTY_SUMMARY);
80
+ const [loading, setLoading] = useState(Boolean(productId));
81
+ const [error, setError] = useState(null);
82
+ useEffect(() => {
83
+ if (!productId) {
84
+ setRating(EMPTY_SUMMARY);
85
+ setLoading(false);
86
+ return;
87
+ }
88
+ let active = true;
89
+ setLoading(true);
90
+ provider.getProductRating(productId).then((result) => {
91
+ if (!active) return;
92
+ setRating(result);
93
+ setError(null);
94
+ }).catch((err) => {
95
+ if (!active) return;
96
+ setError(err instanceof Error ? err : new Error(String(err)));
97
+ setRating(EMPTY_SUMMARY);
98
+ }).finally(() => {
99
+ if (active) setLoading(false);
100
+ });
101
+ return () => {
102
+ active = false;
103
+ };
104
+ }, [provider, productId]);
105
+ return { rating, loading, error };
106
+ }
107
+ function useProductReviews(query) {
108
+ const { provider } = useReputationContext();
109
+ const [reviews, setReviews] = useState([]);
110
+ const [loading, setLoading] = useState(Boolean(query.productId));
111
+ const [error, setError] = useState(null);
112
+ const { productId, limit, minRating, sort } = query;
113
+ useEffect(() => {
114
+ if (!productId) {
115
+ setReviews([]);
116
+ setLoading(false);
117
+ return;
118
+ }
119
+ let active = true;
120
+ setLoading(true);
121
+ provider.listProductReviews({ productId, limit, minRating, sort }).then((result) => {
122
+ if (!active) return;
123
+ setReviews(result);
124
+ setError(null);
125
+ }).catch((err) => {
126
+ if (!active) return;
127
+ setError(err instanceof Error ? err : new Error(String(err)));
128
+ setReviews([]);
129
+ }).finally(() => {
130
+ if (active) setLoading(false);
131
+ });
132
+ return () => {
133
+ active = false;
134
+ };
135
+ }, [provider, productId, limit, minRating, sort]);
136
+ return { reviews, loading, error };
137
+ }
138
+ function useTopReviews(query) {
139
+ const { provider } = useReputationContext();
140
+ const [reviews, setReviews] = useState([]);
141
+ const [loading, setLoading] = useState(true);
142
+ const [error, setError] = useState(null);
143
+ const limit = query?.limit;
144
+ const minRating = query?.minRating;
145
+ const productOnly = query?.productOnly;
146
+ const productKey = query?.productIds?.join(",") ?? "";
147
+ useEffect(() => {
148
+ let active = true;
149
+ setLoading(true);
150
+ provider.listTopReviews({
151
+ limit,
152
+ minRating,
153
+ productOnly,
154
+ productIds: productKey ? productKey.split(",") : void 0
155
+ }).then((result) => {
156
+ if (!active) return;
157
+ setReviews(result);
158
+ setError(null);
159
+ }).catch((err) => {
160
+ if (!active) return;
161
+ setError(err instanceof Error ? err : new Error(String(err)));
162
+ setReviews([]);
163
+ }).finally(() => {
164
+ if (active) setLoading(false);
165
+ });
166
+ return () => {
167
+ active = false;
168
+ };
169
+ }, [provider, limit, minRating, productOnly, productKey]);
170
+ return { reviews, loading, error };
171
+ }
172
+ function Row({ size, color, count = 5 }) {
173
+ return /* @__PURE__ */ jsx(Fragment, { children: Array.from({ length: count }, (_, i) => /* @__PURE__ */ jsx("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: color, "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx("path", { d: "M12 17.3l-6.16 3.73 1.64-7.03L2 9.24l7.19-.61L12 2l2.81 6.63 7.19.61-5.48 4.76 1.64 7.03z" }) }, i)) });
174
+ }
175
+ function Stars2({ value, size = 16, color = "#f59e0b", emptyColor = "#d4d4d8", className, label }) {
176
+ const clamped = Math.max(0, Math.min(5, Number.isFinite(value) ? value : 0));
177
+ const rounded = Math.round(clamped * 2) / 2;
178
+ const wrap2 = { position: "relative", display: "inline-flex", lineHeight: 0, flexShrink: 0 };
179
+ const overlay = {
180
+ position: "absolute",
181
+ inset: 0,
182
+ display: "inline-flex",
183
+ lineHeight: 0,
184
+ overflow: "hidden",
185
+ width: `${rounded / 5 * 100}%`
186
+ };
187
+ return /* @__PURE__ */ jsxs(
188
+ "span",
189
+ {
190
+ className,
191
+ style: wrap2,
192
+ role: "img",
193
+ "aria-label": label === null ? void 0 : label ?? `${rounded} de 5 estrelas`,
194
+ "aria-hidden": label === null ? true : void 0,
195
+ children: [
196
+ /* @__PURE__ */ jsx(Row, { size, color: emptyColor }),
197
+ /* @__PURE__ */ jsx("span", { style: overlay, "aria-hidden": "true", children: /* @__PURE__ */ jsx(Row, { size, color }) })
198
+ ]
199
+ }
200
+ );
201
+ }
202
+ var wrap = { display: "inline-flex", alignItems: "center", gap: "0.4rem", fontSize: "0.875rem" };
203
+ function ProductRating({
204
+ productId,
205
+ average,
206
+ count,
207
+ size = 15,
208
+ hideCount = false,
209
+ emptyLabel,
210
+ className,
211
+ onClick
212
+ }) {
213
+ const fetched = useProductRating(average == null ? productId : void 0);
214
+ const value = average ?? fetched.rating.average;
215
+ const total = count ?? fetched.rating.count;
216
+ if (total === 0) {
217
+ return emptyLabel ? /* @__PURE__ */ jsx("span", { className, style: { ...wrap, opacity: 0.7 }, children: emptyLabel }) : null;
218
+ }
219
+ const label = `${value.toFixed(1).replace(".", ",")} de 5, ${total} ${total === 1 ? "avalia\xE7\xE3o" : "avalia\xE7\xF5es"}`;
220
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [
221
+ /* @__PURE__ */ jsx(Stars2, { value, size, label: null }),
222
+ /* @__PURE__ */ jsx("span", { style: { fontWeight: 600 }, children: value.toFixed(1).replace(".", ",") }),
223
+ !hideCount && /* @__PURE__ */ jsxs("span", { style: { opacity: 0.7 }, children: [
224
+ "(",
225
+ total,
226
+ ")"
227
+ ] })
228
+ ] });
229
+ return onClick ? /* @__PURE__ */ jsx(
230
+ "button",
231
+ {
232
+ type: "button",
233
+ onClick,
234
+ className,
235
+ style: { ...wrap, background: "none", border: 0, padding: 0, cursor: "pointer", font: "inherit" },
236
+ "aria-label": `${label}. Ver avalia\xE7\xF5es.`,
237
+ children: content
238
+ }
239
+ ) : /* @__PURE__ */ jsx("span", { className, style: wrap, role: "img", "aria-label": label, children: content });
240
+ }
241
+ var card = { display: "flex", flexDirection: "column", gap: "0.5rem" };
242
+ function DistributionBar({ stars, count, total }) {
243
+ const pct = total === 0 ? 0 : Math.round(count / total * 100);
244
+ return /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.5rem", fontSize: "0.8125rem" }, children: [
245
+ /* @__PURE__ */ jsxs("span", { style: { width: "1.5rem", textAlign: "right", opacity: 0.7 }, children: [
246
+ stars,
247
+ "\u2605"
248
+ ] }),
249
+ /* @__PURE__ */ jsx(
250
+ "span",
251
+ {
252
+ style: { flex: 1, height: 6, borderRadius: 3, background: "currentColor", opacity: 0.12, position: "relative" },
253
+ "aria-hidden": "true",
254
+ children: /* @__PURE__ */ jsx(
255
+ "span",
256
+ {
257
+ style: { position: "absolute", inset: 0, width: `${pct}%`, borderRadius: 3, background: "#f59e0b", opacity: 1 }
258
+ }
259
+ )
260
+ }
261
+ ),
262
+ /* @__PURE__ */ jsx("span", { style: { width: "2.5rem", opacity: 0.7 }, children: count })
263
+ ] });
264
+ }
265
+ function ReviewCard({ review }) {
266
+ return /* @__PURE__ */ jsxs("article", { style: { ...card, paddingBlock: "1rem", borderTop: "1px solid currentColor", borderTopColor: "rgba(128,128,128,0.25)" }, children: [
267
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: "0.6rem", flexWrap: "wrap" }, children: [
268
+ /* @__PURE__ */ jsx(Stars2, { value: review.rating, size: 14, label: `${review.rating} de 5` }),
269
+ /* @__PURE__ */ jsx("strong", { style: { fontSize: "0.9375rem" }, children: review.author }),
270
+ review.verified && /* @__PURE__ */ jsx(
271
+ "span",
272
+ {
273
+ style: { fontSize: "0.6875rem", letterSpacing: "0.04em", textTransform: "uppercase", padding: "2px 6px", borderRadius: 3, background: "rgba(16,122,90,0.12)", color: "#0d6a58" },
274
+ children: "Compra verificada"
275
+ }
276
+ ),
277
+ review.date && /* @__PURE__ */ jsx("span", { style: { fontSize: "0.8125rem", opacity: 0.6 }, children: review.date })
278
+ ] }),
279
+ review.title && /* @__PURE__ */ jsx("h4", { style: { margin: 0, fontSize: "1rem" }, children: review.title }),
280
+ review.text && /* @__PURE__ */ jsx("p", { style: { margin: 0, lineHeight: 1.6 }, children: review.text }),
281
+ review.replyText && /* @__PURE__ */ jsxs("p", { style: { margin: "0.25rem 0 0", paddingLeft: "0.85rem", borderLeft: "2px solid rgba(128,128,128,0.35)", opacity: 0.85, lineHeight: 1.6 }, children: [
282
+ /* @__PURE__ */ jsx("strong", { style: { display: "block", fontSize: "0.8125rem" }, children: "Resposta da loja" }),
283
+ review.replyText
284
+ ] })
285
+ ] });
286
+ }
287
+ function ProductReviews({
288
+ productId,
289
+ heading = "Avalia\xE7\xF5es",
290
+ limit = 6,
291
+ sort = "recent",
292
+ emptyState,
293
+ className
294
+ }) {
295
+ const { rating } = useProductRating(productId);
296
+ const { reviews, loading } = useProductReviews({ productId, limit, sort });
297
+ if (loading && reviews.length === 0) return null;
298
+ if (rating.count === 0) {
299
+ return emptyState === void 0 ? null : /* @__PURE__ */ jsxs("section", { className, id: "avaliacoes", children: [
300
+ /* @__PURE__ */ jsx("h3", { style: { marginTop: 0 }, children: heading }),
301
+ emptyState
302
+ ] });
303
+ }
304
+ const distribution = rating.distribution ?? [];
305
+ return /* @__PURE__ */ jsxs("section", { className, id: "avaliacoes", "aria-labelledby": "avaliacoes-heading", children: [
306
+ /* @__PURE__ */ jsx("h3", { id: "avaliacoes-heading", style: { marginTop: 0 }, children: heading }),
307
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: "2rem", flexWrap: "wrap", alignItems: "flex-start", marginBottom: "0.5rem" }, children: [
308
+ /* @__PURE__ */ jsxs("div", { style: { ...card, minWidth: "8rem" }, children: [
309
+ /* @__PURE__ */ jsx("span", { style: { fontSize: "2.5rem", fontWeight: 650, lineHeight: 1 }, children: rating.average.toFixed(1).replace(".", ",") }),
310
+ /* @__PURE__ */ jsx(ProductRating, { average: rating.average, count: rating.count })
311
+ ] }),
312
+ distribution.length > 0 && /* @__PURE__ */ jsx("div", { style: { flex: "1 1 16rem", minWidth: "12rem", display: "flex", flexDirection: "column", gap: "0.3rem" }, children: distribution.map((d) => /* @__PURE__ */ jsx(DistributionBar, { stars: d.stars, count: d.count, total: rating.count }, d.stars)) })
313
+ ] }),
314
+ reviews.map((review) => /* @__PURE__ */ jsx(ReviewCard, { review }, review.id))
315
+ ] });
316
+ }
317
+ var grid = {
318
+ display: "grid",
319
+ gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 16rem), 1fr))",
320
+ gap: "1rem"
321
+ };
322
+ var cardStyle = {
323
+ display: "flex",
324
+ flexDirection: "column",
325
+ gap: "0.6rem",
326
+ padding: "1.25rem",
327
+ border: "1px solid rgba(128,128,128,0.25)",
328
+ borderRadius: 4,
329
+ height: "100%",
330
+ color: "inherit",
331
+ textDecoration: "none"
332
+ };
333
+ function Card({ review, href }) {
334
+ const body = /* @__PURE__ */ jsxs(Fragment, { children: [
335
+ /* @__PURE__ */ jsx(Stars2, { value: review.rating, size: 15, label: `${review.rating} de 5` }),
336
+ review.title && /* @__PURE__ */ jsx("strong", { style: { fontSize: "1rem" }, children: review.title }),
337
+ review.text && /* @__PURE__ */ jsx("p", { style: { margin: 0, lineHeight: 1.6, flex: 1 }, children: review.text }),
338
+ /* @__PURE__ */ jsxs("footer", { style: { fontSize: "0.8125rem", opacity: 0.75, display: "flex", gap: "0.4rem", flexWrap: "wrap" }, children: [
339
+ /* @__PURE__ */ jsx("span", { children: review.author }),
340
+ review.productName && /* @__PURE__ */ jsxs(Fragment, { children: [
341
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
342
+ /* @__PURE__ */ jsx("span", { children: review.productName })
343
+ ] })
344
+ ] })
345
+ ] });
346
+ return href ? /* @__PURE__ */ jsx("a", { href, style: cardStyle, children: body }) : /* @__PURE__ */ jsx("article", { style: cardStyle, children: body });
347
+ }
348
+ function TopReviews({
349
+ heading = "O que dizem nossos clientes",
350
+ subheading,
351
+ productHref,
352
+ emptyState,
353
+ className,
354
+ ...query
355
+ }) {
356
+ const { reviews, loading } = useTopReviews(query);
357
+ if (loading && reviews.length === 0) return null;
358
+ if (reviews.length === 0) return emptyState === void 0 ? null : /* @__PURE__ */ jsx(Fragment, { children: emptyState });
359
+ return /* @__PURE__ */ jsxs("section", { className, "aria-labelledby": "top-reviews-heading", children: [
360
+ /* @__PURE__ */ jsx("h2", { id: "top-reviews-heading", style: { marginTop: 0 }, children: heading }),
361
+ subheading && /* @__PURE__ */ jsx("p", { style: { marginTop: "-0.5rem", opacity: 0.75 }, children: subheading }),
362
+ /* @__PURE__ */ jsx("div", { style: grid, children: reviews.map((review) => /* @__PURE__ */ jsx(Card, { review, href: productHref?.(review) }, review.id)) })
363
+ ] });
364
+ }
154
365
 
155
366
  // src/public/index.tsx
156
367
  function createReputationWebsite(options) {
157
368
  const basePath = options?.basePath ?? "/reviews";
158
369
  const heading = options?.heading ?? "O que os pacientes dizem";
159
370
  const provider = options?.dataProvider ?? createSafeDataProvider(
160
- () => createSupabaseReputationProvider(),
371
+ () => createSupabaseReputationProvider({ tenantId: options?.tenantId }),
161
372
  () => createMockReputationProvider({ seed: options?.seed })
162
373
  );
163
374
  const value = { provider };
@@ -182,6 +393,6 @@ function createReputationWebsite(options) {
182
393
  return { manifest, Provider, dataProvider: provider };
183
394
  }
184
395
 
185
- export { ReputationProvider, ReviewsList, createMockReputationProvider, createReputationWebsite, createSupabaseReputationProvider, useReputationContext, useReviewSummary, useReviews };
396
+ export { ProductRating, ProductReviews, ReviewsList, Stars2 as Stars, TopReviews, createReputationWebsite, useProductRating, useProductReviews, useReviews, useTopReviews };
186
397
  //# sourceMappingURL=index.js.map
187
398
  //# sourceMappingURL=index.js.map