@fayz-ai/plugin-reputation 0.9.0-next.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/package.json +4 -7
- package/src/components/ReviewsList.tsx +0 -69
- package/src/context.tsx +0 -20
- package/src/data/index.ts +0 -4
- package/src/data/mock.ts +0 -45
- package/src/data/supabase.ts +0 -24
- package/src/data/types.ts +0 -10
- package/src/hooks/index.ts +0 -4
- package/src/hooks/useReviewSummary.ts +0 -41
- package/src/hooks/useReviews.ts +0 -44
- package/src/index.ts +0 -56
- package/src/public/index.tsx +0 -82
- package/src/types.ts +0 -34
- package/src/views/ReputationHome.tsx +0 -102
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/
|
|
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.
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"fayz": {
|
|
4
4
|
"status": "preview"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.9.
|
|
6
|
+
"version": "0.9.1",
|
|
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/
|
|
36
|
-
"@fayz-ai/
|
|
32
|
+
"@fayz-ai/core": "^0.10.0",
|
|
33
|
+
"@fayz-ai/ui": "^0.10.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
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
|
-
}
|
package/src/data/supabase.ts
DELETED
|
@@ -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
|
-
}
|
package/src/hooks/index.ts
DELETED
|
@@ -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
|
-
}
|
package/src/hooks/useReviews.ts
DELETED
|
@@ -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
|
-
}
|
package/src/public/index.tsx
DELETED
|
@@ -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
|
-
}
|