@fayz-ai/plugin-reputation 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @fayz-ai/plugin-reputation
2
2
 
3
- > **Status: experimental (incubating).** Not capability-complete — missing some or all of the capability bar (data-provider contract w/ supabase+mock pair, entity registries, settings, migrations; see `docs/PLUGIN-PATTERNS.md`). Fine to explore in dogfoods; NOT ready for fresh installs or generated apps, and its API may change without notice.
3
+ > **Status: experimental (incubating).** Not capability-complete — missing some or all of the capability bar (data-provider contract w/ supabase+mock pair, entity registries, settings, migrations; see `docs/plugins/PLUGIN-PATTERNS.md`). Fine to explore in dogfoods; NOT ready for fresh installs or generated apps, and its API may change without notice.
4
4
 
5
5
  > Reviews and reputation, in one place — see the stars, reply, ask for more.
6
6
 
@@ -25,7 +25,7 @@ Peer deps: `@fayz-ai/core`, `@fayz-ai/ui`, plus `react` / `react-dom`.
25
25
 
26
26
  ## Usage
27
27
  ```tsx
28
- import { defineSaas } from '@fayz-ai/saas'
28
+ import { defineSaas } from '@fayz-ai/admin'
29
29
  import { createReputationPlugin } from '@fayz-ai/plugin-reputation'
30
30
 
31
31
  export const app = defineSaas({
@@ -40,4 +40,4 @@ export const app = defineSaas({
40
40
  The trust layer of the engine. Pairs with `plugin-conversations` (turn a great chat into a review request), `plugin-crm` (know exactly who to ask), and `plugin-marketing` (feed the social proof back into acquisition).
41
41
 
42
42
  ## Roadmap & contributing
43
- Early scaffold — built in the open. See the [Fayz SDK roadmap](../../docs/ROADMAP.md#plugin-reputation) for current gaps, missing features, and good first issues.
43
+ Early scaffold — built in the open. See the [Fayz SDK roadmap](../../docs/platform/ROADMAP.md#plugin-reputation) for current gaps, missing features, and good first issues.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAa5E,MAAM,WAAW,uBAAuB;IACtC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB;AAED,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,cAAc,CAiCxF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAa5E,MAAM,WAAW,uBAAuB;IACtC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;CACxB;AAED,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,cAAc,CAoCxF"}
package/dist/index.js CHANGED
@@ -102,7 +102,10 @@ function createReputationPlugin(options) {
102
102
  permission: { feature: "reputation", action: "read" }
103
103
  }
104
104
  ],
105
- widgets: []
105
+ widgets: [],
106
+ // How the review request reaches the customer — the two the Fayz plugin
107
+ // page lists for Reputação.
108
+ integrations: ["whatsapp", "twilio"]
106
109
  };
107
110
  }
108
111
 
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 }\n}\n"]}
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"]}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "fayz": {
4
4
  "status": "preview"
5
5
  },
6
- "version": "0.9.0",
6
+ "version": "0.9.2",
7
7
  "description": "[experimental] Fayz SDK — reviews & reputation management plugin",
8
8
  "type": "module",
9
9
  "sideEffects": false,
@@ -12,19 +12,16 @@
12
12
  "types": "./dist/index.d.ts",
13
13
  "exports": {
14
14
  ".": {
15
- "source": "./src/index.ts",
16
15
  "types": "./dist/index.d.ts",
17
16
  "import": "./dist/index.js"
18
17
  },
19
18
  "./public": {
20
- "source": "./src/public/index.tsx",
21
19
  "types": "./dist/public/index.d.ts",
22
20
  "import": "./dist/public/index.js"
23
21
  }
24
22
  },
25
23
  "files": [
26
- "dist",
27
- "src"
24
+ "dist"
28
25
  ],
29
26
  "peerDependencies": {
30
27
  "react": "^18.0.0 || ^19.0.0",
@@ -32,8 +29,8 @@
32
29
  "lucide-react": ">=0.400.0 <1.0.0"
33
30
  },
34
31
  "dependencies": {
35
- "@fayz-ai/core": "^0.9.0",
36
- "@fayz-ai/ui": "^0.9.0"
32
+ "@fayz-ai/core": "^0.11.0",
33
+ "@fayz-ai/ui": "^0.11.0"
37
34
  },
38
35
  "devDependencies": {
39
36
  "@types/react": "^18.3.0",
@@ -1,69 +0,0 @@
1
- import { Star } from 'lucide-react'
2
- import { useReviews } from '../hooks/useReviews'
3
- import { useReviewSummary } from '../hooks/useReviewSummary'
4
-
5
- function Stars({ rating }: { rating: number }) {
6
- return (
7
- <div className="flex items-center gap-0.5">
8
- {[1, 2, 3, 4, 5].map((i) => (
9
- <Star
10
- key={i}
11
- className={`h-4 w-4 ${i <= rating ? 'fill-primary text-primary' : 'text-muted-foreground/30'}`}
12
- />
13
- ))}
14
- </div>
15
- )
16
- }
17
-
18
- /**
19
- * Default reviews list with an aggregate header — token-only so it inherits the
20
- * host theme. Optional: a host with bespoke review cards can ignore this and map
21
- * `useReviews()` / `useReviewSummary()` itself (as hempdent does on its home).
22
- */
23
- export function ReviewsList({ limit, heading }: { limit?: number; heading?: string }) {
24
- const { reviews, loading } = useReviews({ limit })
25
- const { summary } = useReviewSummary()
26
-
27
- return (
28
- <section className="py-16 bg-background">
29
- <div className="container mx-auto px-6">
30
- <div className="text-center mb-12">
31
- {heading ? (
32
- <h1 className="font-heading text-4xl md:text-5xl font-bold text-foreground mb-3">{heading}</h1>
33
- ) : null}
34
- {summary ? (
35
- <div className="flex items-center justify-center gap-2">
36
- <Stars rating={Math.round(summary.average)} />
37
- <span className="text-foreground font-semibold text-lg">{summary.average}</span>
38
- <span className="text-muted-foreground text-sm">· {summary.count} avaliações</span>
39
- </div>
40
- ) : null}
41
- </div>
42
-
43
- {loading ? (
44
- <p className="text-center text-muted-foreground">Carregando avaliações…</p>
45
- ) : (
46
- <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
47
- {reviews.map((review) => (
48
- <div key={review.id} className="rounded-2xl bg-card border border-border p-6 shadow-sm">
49
- <div className="mb-3">
50
- <Stars rating={review.rating} />
51
- </div>
52
- <p className="text-foreground text-sm leading-relaxed mb-4">"{review.text}"</p>
53
- <div className="flex items-center justify-between">
54
- <div className="flex items-center gap-2">
55
- <div className="h-8 w-8 rounded-full bg-accent flex items-center justify-center text-primary font-semibold text-sm">
56
- {review.author[0]}
57
- </div>
58
- <span className="text-sm font-medium text-foreground">{review.author}</span>
59
- </div>
60
- <span className="text-xs text-muted-foreground">{review.date}</span>
61
- </div>
62
- </div>
63
- ))}
64
- </div>
65
- )}
66
- </div>
67
- </section>
68
- )
69
- }
package/src/context.tsx DELETED
@@ -1,20 +0,0 @@
1
- import { createContext, useContext, type ReactNode } from 'react'
2
- import type { ReputationDataProvider } from './data/types'
3
-
4
- export interface ReputationContextValue {
5
- provider: ReputationDataProvider
6
- }
7
-
8
- const ReputationContext = createContext<ReputationContextValue | null>(null)
9
-
10
- export function ReputationProvider({ value, children }: { value: ReputationContextValue; children: ReactNode }) {
11
- return <ReputationContext.Provider value={value}>{children}</ReputationContext.Provider>
12
- }
13
-
14
- export function useReputationContext(): ReputationContextValue {
15
- const ctx = useContext(ReputationContext)
16
- if (!ctx) {
17
- throw new Error('[plugin-reputation] useReputationContext must be used within <ReputationProvider>.')
18
- }
19
- return ctx
20
- }
package/src/data/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export type { ReputationDataProvider } from './types'
2
- export { createMockReputationProvider } from './mock'
3
- export type { ReputationSeed, MockReputationProviderOptions } from './mock'
4
- export { createSupabaseReputationProvider } from './supabase'
package/src/data/mock.ts DELETED
@@ -1,45 +0,0 @@
1
- import type { ReputationDataProvider } from './types'
2
- import type { Review, ReviewSummary, ReviewListQuery } from '../types'
3
-
4
- export interface ReputationSeed {
5
- reviews: Review[]
6
- /** Explicit aggregate. If omitted, it is computed from `reviews`. */
7
- summary?: ReviewSummary
8
- }
9
-
10
- export interface MockReputationProviderOptions {
11
- seed?: ReputationSeed
12
- }
13
-
14
- const FALLBACK_REVIEWS: Review[] = [
15
- { id: 'r1', author: 'Camila R.', source: 'Google', rating: 5, text: 'Excelente atendimento, recomendo!', date: 'Jun 2025' },
16
- { id: 'r2', author: 'Tom B.', source: 'Facebook', rating: 5, text: 'Processo simples e resultado ótimo.', date: 'Jun 2025' },
17
- { id: 'r3', author: 'Aisha K.', source: 'Google', rating: 4, text: 'Muito bom no geral.', date: 'Jun 2025' },
18
- ]
19
-
20
- function computeSummary(reviews: Review[]): ReviewSummary {
21
- const count = reviews.length
22
- const average = count === 0 ? 0 : Math.round((reviews.reduce((s, r) => s + r.rating, 0) / count) * 10) / 10
23
- const distribution = [5, 4, 3, 2, 1].map((stars) => ({
24
- stars,
25
- count: reviews.filter((r) => Math.round(r.rating) === stars).length,
26
- }))
27
- return { average, count, distribution }
28
- }
29
-
30
- export function createMockReputationProvider(options?: MockReputationProviderOptions): ReputationDataProvider {
31
- const reviews: Review[] = options?.seed?.reviews ?? FALLBACK_REVIEWS
32
- const summary: ReviewSummary = options?.seed?.summary ?? computeSummary(reviews)
33
-
34
- return {
35
- async listReviews(query?: ReviewListQuery): Promise<Review[]> {
36
- let result = reviews
37
- if (query?.minRating != null) result = result.filter((r) => r.rating >= query.minRating!)
38
- if (query?.limit != null) result = result.slice(0, query.limit)
39
- return result
40
- },
41
- async getSummary(): Promise<ReviewSummary> {
42
- return summary
43
- },
44
- }
45
- }
@@ -1,24 +0,0 @@
1
- import type { ReputationDataProvider } from './types'
2
- import type { Review, ReviewSummary, ReviewListQuery } from '../types'
3
-
4
- // ---------------------------------------------------------------------------
5
- // Supabase-backed reputation provider — STUB (deferred to Phase 2).
6
- //
7
- // Later: read a `reviews` table (tenant-scoped, RLS) and/or synced Google/
8
- // Facebook reviews. Swapping this in is a pure provider change — hooks and
9
- // components are untouched. Throws until then so createSafeDataProvider falls
10
- // back to the mock/seed provider whenever no Supabase client is configured.
11
- // ---------------------------------------------------------------------------
12
-
13
- export function createSupabaseReputationProvider(): ReputationDataProvider {
14
- const notImplemented = (): never => {
15
- throw new Error(
16
- '[plugin-reputation] Supabase provider not implemented yet — deferred to Phase 2. ' +
17
- 'Run on the mock/seed provider (no Supabase client configured) for now.',
18
- )
19
- }
20
- return {
21
- listReviews: (_query?: ReviewListQuery): Promise<Review[]> => notImplemented(),
22
- getSummary: (): Promise<ReviewSummary> => notImplemented(),
23
- }
24
- }
package/src/data/types.ts DELETED
@@ -1,10 +0,0 @@
1
- import type { Review, ReviewSummary, ReviewListQuery } from '../types'
2
-
3
- /**
4
- * Data seam for reputation. Mock/seed powers the POC; a Supabase (or
5
- * Google/Facebook sync) implementation swaps in later with no component change.
6
- */
7
- export interface ReputationDataProvider {
8
- listReviews(query?: ReviewListQuery): Promise<Review[]>
9
- getSummary(): Promise<ReviewSummary>
10
- }
@@ -1,4 +0,0 @@
1
- export { useReviews } from './useReviews'
2
- export type { UseReviewsResult } from './useReviews'
3
- export { useReviewSummary } from './useReviewSummary'
4
- export type { UseReviewSummaryResult } from './useReviewSummary'
@@ -1,41 +0,0 @@
1
- import { useEffect, useState } from 'react'
2
- import { useReputationContext } from '../context'
3
- import type { ReviewSummary } from '../types'
4
-
5
- export interface UseReviewSummaryResult {
6
- summary: ReviewSummary | null
7
- loading: boolean
8
- error: Error | null
9
- }
10
-
11
- /** Fetch the aggregate rating summary (average + count + distribution). */
12
- export function useReviewSummary(): UseReviewSummaryResult {
13
- const { provider } = useReputationContext()
14
- const [summary, setSummary] = useState<ReviewSummary | null>(null)
15
- const [loading, setLoading] = useState(true)
16
- const [error, setError] = useState<Error | null>(null)
17
-
18
- useEffect(() => {
19
- let active = true
20
- setLoading(true)
21
- provider
22
- .getSummary()
23
- .then((result) => {
24
- if (!active) return
25
- setSummary(result)
26
- setError(null)
27
- })
28
- .catch((err) => {
29
- if (!active) return
30
- setError(err instanceof Error ? err : new Error(String(err)))
31
- })
32
- .finally(() => {
33
- if (active) setLoading(false)
34
- })
35
- return () => {
36
- active = false
37
- }
38
- }, [provider])
39
-
40
- return { summary, loading, error }
41
- }
@@ -1,44 +0,0 @@
1
- import { useEffect, useState } from 'react'
2
- import { useReputationContext } from '../context'
3
- import type { Review, ReviewListQuery } from '../types'
4
-
5
- export interface UseReviewsResult {
6
- reviews: Review[]
7
- loading: boolean
8
- error: Error | null
9
- }
10
-
11
- /** Fetch the review list from the active reputation provider. */
12
- export function useReviews(query?: ReviewListQuery): UseReviewsResult {
13
- const { provider } = useReputationContext()
14
- const [reviews, setReviews] = useState<Review[]>([])
15
- const [loading, setLoading] = useState(true)
16
- const [error, setError] = useState<Error | null>(null)
17
-
18
- const limit = query?.limit
19
- const minRating = query?.minRating
20
-
21
- useEffect(() => {
22
- let active = true
23
- setLoading(true)
24
- provider
25
- .listReviews({ limit, minRating })
26
- .then((result) => {
27
- if (!active) return
28
- setReviews(result)
29
- setError(null)
30
- })
31
- .catch((err) => {
32
- if (!active) return
33
- setError(err instanceof Error ? err : new Error(String(err)))
34
- })
35
- .finally(() => {
36
- if (active) setLoading(false)
37
- })
38
- return () => {
39
- active = false
40
- }
41
- }, [provider, limit, minRating])
42
-
43
- return { reviews, loading, error }
44
- }
package/src/index.ts DELETED
@@ -1,56 +0,0 @@
1
- import React from 'react'
2
- import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
3
- import { ReputationHome } from './views/ReputationHome'
4
-
5
- // ---------------------------------------------------------------------------
6
- // @fayz-ai/plugin-reputation — reviews & reputation (GoHighLevel "Reputation").
7
- // Universal plugin. M1 ships a rich mock home; Google/Facebook review sync +
8
- // automated review requests (over connectors + automations) come later.
9
- //
10
- // The public/website surface (createReputationWebsite + hooks + ReviewsList)
11
- // lives in the lean './public' subpath so a marketing-site host imports it
12
- // WITHOUT pulling the admin view's @fayz-ai/ui graph.
13
- // ---------------------------------------------------------------------------
14
-
15
- export interface ReputationPluginOptions {
16
- navPosition?: number
17
- navSection?: 'main' | 'secondary' | 'settings'
18
- navLabel?: string
19
- scope?: PluginScope
20
- verticalId?: VerticalId
21
- }
22
-
23
- export function createReputationPlugin(options?: ReputationPluginOptions): PluginManifest {
24
- const PageComponent: React.ComponentType<unknown> = () => React.createElement(ReputationHome)
25
- PageComponent.displayName = 'ReputationHome'
26
-
27
- return {
28
- id: 'reputation',
29
- name: options?.navLabel ?? 'Reputation',
30
- icon: 'Award',
31
- version: '1.0.0',
32
- scope: options?.scope ?? 'universal',
33
- verticalId: options?.verticalId,
34
- defaultEnabled: true,
35
- dependencies: [],
36
- declaredFeatures: [{ id: 'reputation', label: 'Reputation', group: 'Retain' }],
37
- navigation: [
38
- {
39
- section: options?.navSection ?? 'main',
40
- position: options?.navPosition ?? 8,
41
- label: options?.navLabel ?? 'Reputation',
42
- route: '/reputation',
43
- icon: 'Award',
44
- permission: { feature: 'reputation', action: 'read' as const },
45
- },
46
- ],
47
- routes: [
48
- {
49
- path: '/reputation',
50
- component: PageComponent,
51
- permission: { feature: 'reputation', action: 'read' as const },
52
- },
53
- ],
54
- widgets: [],
55
- }
56
- }
@@ -1,82 +0,0 @@
1
- import { createElement, type FC, type ReactNode } from 'react'
2
- import type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'
3
- import { createSafeDataProvider } from '@fayz-ai/core'
4
- import { createMockReputationProvider, type ReputationSeed } from '../data/mock'
5
- import { createSupabaseReputationProvider } from '../data/supabase'
6
- import { ReputationProvider, type ReputationContextValue } from '../context'
7
- import { ReviewsList } from '../components/ReviewsList'
8
- import type { ReputationDataProvider } from '../data/types'
9
-
10
- // ---------------------------------------------------------------------------
11
- // @fayz-ai/plugin-reputation/public — website reviews surface.
12
- //
13
- // Lean entry (no admin ReputationHome / @fayz-ai/ui). Returns a { manifest,
14
- // Provider } bundle the host reads uniformly: the Provider wraps the app root so
15
- // useReviews()/useReviewSummary() power the host's own review markup, and
16
- // manifest.routes ships an optional /reviews page.
17
- // ---------------------------------------------------------------------------
18
-
19
- export interface ReputationWebsiteOptions {
20
- /** Base path for the optional public "all reviews" page. Default '/reviews'. */
21
- basePath?: string
22
- /** Seed reviews + summary for the mock provider (used until a real backend is wired). */
23
- seed?: ReputationSeed
24
- /** Inject a custom provider. Overrides the safe mock/Supabase resolver. */
25
- dataProvider?: ReputationDataProvider
26
- /** Heading for the optional /reviews screen. */
27
- heading?: string
28
- scope?: PluginScope
29
- verticalId?: VerticalId
30
- }
31
-
32
- export interface ReputationWebsitePlugin {
33
- manifest: PluginManifest
34
- Provider: FC<{ children: ReactNode }>
35
- dataProvider: ReputationDataProvider
36
- }
37
-
38
- export function createReputationWebsite(options?: ReputationWebsiteOptions): ReputationWebsitePlugin {
39
- const basePath = options?.basePath ?? '/reviews'
40
- const heading = options?.heading ?? 'O que os pacientes dizem'
41
- const provider =
42
- options?.dataProvider ??
43
- createSafeDataProvider(
44
- () => createSupabaseReputationProvider(),
45
- () => createMockReputationProvider({ seed: options?.seed }),
46
- )
47
-
48
- const value: ReputationContextValue = { provider }
49
- const Provider: FC<{ children: ReactNode }> = ({ children }) =>
50
- createElement(ReputationProvider, { value, children })
51
- Provider.displayName = 'ReputationWebsiteProvider'
52
-
53
- const ReviewsScreen: FC<unknown> = () => createElement(ReviewsList, { heading })
54
- ReviewsScreen.displayName = 'ReviewsScreen'
55
-
56
- const manifest: PluginManifest = {
57
- id: 'reputation',
58
- name: 'Reviews',
59
- icon: 'Star',
60
- version: '0.1.0',
61
- scope: options?.scope ?? 'universal',
62
- verticalId: options?.verticalId,
63
- scaffolds: ['website', 'landing_page'],
64
- defaultEnabled: true,
65
- dependencies: [],
66
- navigation: [],
67
- routes: [{ path: basePath, component: ReviewsScreen, guard: 'public' }],
68
- widgets: [],
69
- }
70
-
71
- return { manifest, Provider, dataProvider: provider }
72
- }
73
-
74
- // Public API — website surface
75
- export type { Review, ReviewSummary, ReviewSource, ReviewListQuery } from '../types'
76
- export type { ReputationDataProvider } from '../data/types'
77
- export { createMockReputationProvider, createSupabaseReputationProvider } from '../data'
78
- export type { ReputationSeed } from '../data/mock'
79
- export { ReputationProvider, useReputationContext } from '../context'
80
- export type { ReputationContextValue } from '../context'
81
- export { useReviews, useReviewSummary } from '../hooks'
82
- export { ReviewsList } from '../components/ReviewsList'
package/src/types.ts DELETED
@@ -1,34 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // Reputation domain types (shared by the admin M1 view and the website surface).
3
- // Presentation-agnostic: a host site maps these into its own review-card markup.
4
- // ---------------------------------------------------------------------------
5
-
6
- export type ReviewSource = 'Google' | 'Facebook' | 'Instagram' | 'Website' | (string & {})
7
-
8
- export interface Review {
9
- id: string
10
- /** Reviewer display name. */
11
- author: string
12
- rating: number
13
- text: string
14
- /** Human-readable date (e.g. "Janeiro 2025"). */
15
- date: string
16
- source?: ReviewSource
17
- /** Whether the business has replied (admin surface). */
18
- replied?: boolean
19
- }
20
-
21
- export interface ReviewSummary {
22
- /** Average rating, e.g. 4.8. */
23
- average: number
24
- /** Total number of ratings, e.g. 123. */
25
- count: number
26
- /** Optional star distribution (5→1). */
27
- distribution?: Array<{ stars: number; count: number }>
28
- }
29
-
30
- export interface ReviewListQuery {
31
- limit?: number
32
- /** Minimum rating to include. */
33
- minRating?: number
34
- }
@@ -1,102 +0,0 @@
1
- import React from 'react'
2
- import { Star, Send, MessageSquare } from 'lucide-react'
3
- import { Button, PageHeaderActions } from '@fayz-ai/ui'
4
-
5
- interface Review {
6
- id: string
7
- author: string
8
- source: 'Google' | 'Facebook'
9
- rating: number
10
- text: string
11
- date: string
12
- replied: boolean
13
- }
14
-
15
- const REVIEWS: Review[] = [
16
- { 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 },
17
- { id: '2', author: 'Tom B.', source: 'Facebook', rating: 5, text: 'Booking was effortless and the results were fantastic.', date: 'Jun 13', replied: false },
18
- { id: '3', author: 'Aisha K.', source: 'Google', rating: 4, text: 'Great service overall, slightly long wait but worth it.', date: 'Jun 11', replied: false },
19
- { id: '4', author: 'Marco P.', source: 'Google', rating: 5, text: 'Best in town. Highly recommend to anyone.', date: 'Jun 9', replied: true },
20
- { id: '5', author: 'Lena S.', source: 'Facebook', rating: 3, text: 'Good but the pricing could be clearer up front.', date: 'Jun 6', replied: false },
21
- ]
22
-
23
- const DISTRIBUTION = [
24
- { stars: 5, count: 182 },
25
- { stars: 4, count: 41 },
26
- { stars: 3, count: 12 },
27
- { stars: 2, count: 4 },
28
- { stars: 1, count: 3 },
29
- ]
30
-
31
- function Stars({ rating, className }: { rating: number; className?: string }) {
32
- return (
33
- <div className={`flex items-center gap-0.5 ${className ?? ''}`}>
34
- {[1, 2, 3, 4, 5].map((i) => (
35
- <Star key={i} className={`h-4 w-4 ${i <= rating ? 'fill-amber-400 text-amber-400' : 'text-muted-foreground/30'}`} />
36
- ))}
37
- </div>
38
- )
39
- }
40
-
41
- export function ReputationHome() {
42
- const total = DISTRIBUTION.reduce((s, d) => s + d.count, 0)
43
- return (
44
- <div className="mx-auto max-w-6xl p-6">
45
- <PageHeaderActions>
46
- <Button><Send className="mr-1.5 h-4 w-4" /> Request reviews</Button>
47
- </PageHeaderActions>
48
-
49
- <div className="mt-6 grid grid-cols-1 gap-4 lg:grid-cols-3">
50
- <div className="rounded-card border border-border bg-card p-5 text-center">
51
- <p className="text-4xl font-bold text-foreground">4.8</p>
52
- <Stars rating={5} className="mt-1 justify-center" />
53
- <p className="mt-1 text-xs text-muted-foreground">{total} reviews</p>
54
- </div>
55
- <div className="rounded-card border border-border bg-card p-5 lg:col-span-2">
56
- <div className="space-y-1.5">
57
- {DISTRIBUTION.map((d) => (
58
- <div key={d.stars} className="flex items-center gap-2 text-xs">
59
- <span className="w-6 text-muted-foreground">{d.stars}★</span>
60
- <div className="h-2 flex-1 overflow-hidden rounded-full bg-muted">
61
- <div className="h-full rounded-full bg-amber-400" style={{ width: `${(d.count / total) * 100}%` }} />
62
- </div>
63
- <span className="w-8 text-right text-muted-foreground">{d.count}</span>
64
- </div>
65
- ))}
66
- </div>
67
- </div>
68
- </div>
69
-
70
- <div className="mt-6 space-y-3">
71
- {REVIEWS.map((r) => (
72
- <div key={r.id} className="rounded-card border border-border bg-card p-4">
73
- <div className="flex items-start justify-between">
74
- <div className="flex items-center gap-3">
75
- <div className="flex h-9 w-9 items-center justify-center rounded-full bg-muted text-sm font-semibold text-foreground">
76
- {r.author[0]}
77
- </div>
78
- <div>
79
- <p className="text-sm font-semibold text-foreground">{r.author}</p>
80
- <p className="text-xs text-muted-foreground">{r.source} · {r.date}</p>
81
- </div>
82
- </div>
83
- <Stars rating={r.rating} />
84
- </div>
85
- <p className="mt-2 text-sm text-muted-foreground">{r.text}</p>
86
- <div className="mt-2">
87
- {r.replied ? (
88
- <span className="text-xs font-medium text-emerald-600">✓ Replied</span>
89
- ) : (
90
- <Button variant="outline" size="sm"><MessageSquare className="mr-1.5 h-3.5 w-3.5" /> Reply</Button>
91
- )}
92
- </div>
93
- </div>
94
- ))}
95
- </div>
96
-
97
- <p className="mt-4 text-center text-xs text-muted-foreground">
98
- Mock preview · Google/Facebook review sync + automated requests ship in a later milestone.
99
- </p>
100
- </div>
101
- )
102
- }