@fayz-ai/plugin-reputation 0.2.2 → 0.2.4
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 +2 -0
- package/dist/components/ReviewsList.d.ts +10 -0
- package/dist/components/ReviewsList.d.ts.map +1 -0
- package/dist/context.d.ts +11 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/mock.d.ts +12 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +3 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/types.d.ts +10 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/useReviewSummary.d.ts +9 -0
- package/dist/hooks/useReviewSummary.d.ts.map +1 -0
- package/dist/hooks/useReviews.d.ts +9 -0
- package/dist/hooks/useReviews.d.ts.map +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/public/index.cjs +196 -0
- package/dist/public/index.cjs.map +1 -0
- package/dist/public/index.d.ts +33 -0
- package/dist/public/index.d.ts.map +1 -0
- package/dist/public/index.js +187 -0
- package/dist/public/index.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +10 -7
- package/src/components/ReviewsList.tsx +69 -0
- package/src/context.tsx +20 -0
- package/src/data/index.ts +4 -0
- package/src/data/mock.ts +45 -0
- package/src/data/supabase.ts +24 -0
- package/src/data/types.ts +10 -0
- package/src/hooks/index.ts +4 -0
- package/src/hooks/useReviewSummary.ts +41 -0
- package/src/hooks/useReviews.ts +44 -0
- package/src/index.ts +4 -0
- package/src/public/index.tsx +82 -0
- package/src/types.ts +34 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useEffect, createElement } from 'react';
|
|
2
|
+
import { createSafeDataProvider } from '@fayz-ai/core';
|
|
3
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
import { Star } from 'lucide-react';
|
|
5
|
+
|
|
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
|
+
function useReviews(query) {
|
|
63
|
+
const { provider } = useReputationContext();
|
|
64
|
+
const [reviews, setReviews] = useState([]);
|
|
65
|
+
const [loading, setLoading] = useState(true);
|
|
66
|
+
const [error, setError] = useState(null);
|
|
67
|
+
const limit = query?.limit;
|
|
68
|
+
const minRating = query?.minRating;
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
let active = true;
|
|
71
|
+
setLoading(true);
|
|
72
|
+
provider.listReviews({ limit, minRating }).then((result) => {
|
|
73
|
+
if (!active) return;
|
|
74
|
+
setReviews(result);
|
|
75
|
+
setError(null);
|
|
76
|
+
}).catch((err) => {
|
|
77
|
+
if (!active) return;
|
|
78
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
79
|
+
}).finally(() => {
|
|
80
|
+
if (active) setLoading(false);
|
|
81
|
+
});
|
|
82
|
+
return () => {
|
|
83
|
+
active = false;
|
|
84
|
+
};
|
|
85
|
+
}, [provider, limit, minRating]);
|
|
86
|
+
return { reviews, loading, error };
|
|
87
|
+
}
|
|
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
|
+
function Stars({ rating }) {
|
|
113
|
+
return /* @__PURE__ */ jsx("div", { className: "flex items-center gap-0.5", children: [1, 2, 3, 4, 5].map((i) => /* @__PURE__ */ jsx(
|
|
114
|
+
Star,
|
|
115
|
+
{
|
|
116
|
+
className: `h-4 w-4 ${i <= rating ? "fill-primary text-primary" : "text-muted-foreground/30"}`
|
|
117
|
+
},
|
|
118
|
+
i
|
|
119
|
+
)) });
|
|
120
|
+
}
|
|
121
|
+
function ReviewsList({ limit, heading }) {
|
|
122
|
+
const { reviews, loading } = useReviews({ limit });
|
|
123
|
+
const { summary } = useReviewSummary();
|
|
124
|
+
return /* @__PURE__ */ jsx("section", { className: "py-16 bg-background", children: /* @__PURE__ */ jsxs("div", { className: "container mx-auto px-6", children: [
|
|
125
|
+
/* @__PURE__ */ jsxs("div", { className: "text-center mb-12", children: [
|
|
126
|
+
heading ? /* @__PURE__ */ jsx("h1", { className: "font-heading text-4xl md:text-5xl font-bold text-foreground mb-3", children: heading }) : null,
|
|
127
|
+
summary ? /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-center gap-2", children: [
|
|
128
|
+
/* @__PURE__ */ jsx(Stars, { rating: Math.round(summary.average) }),
|
|
129
|
+
/* @__PURE__ */ jsx("span", { className: "text-foreground font-semibold text-lg", children: summary.average }),
|
|
130
|
+
/* @__PURE__ */ jsxs("span", { className: "text-muted-foreground text-sm", children: [
|
|
131
|
+
"\xB7 ",
|
|
132
|
+
summary.count,
|
|
133
|
+
" avalia\xE7\xF5es"
|
|
134
|
+
] })
|
|
135
|
+
] }) : null
|
|
136
|
+
] }),
|
|
137
|
+
loading ? /* @__PURE__ */ jsx("p", { className: "text-center text-muted-foreground", children: "Carregando avalia\xE7\xF5es\u2026" }) : /* @__PURE__ */ jsx("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-6", children: reviews.map((review) => /* @__PURE__ */ jsxs("div", { className: "rounded-2xl bg-card border border-border p-6 shadow-sm", children: [
|
|
138
|
+
/* @__PURE__ */ jsx("div", { className: "mb-3", children: /* @__PURE__ */ jsx(Stars, { rating: review.rating }) }),
|
|
139
|
+
/* @__PURE__ */ jsxs("p", { className: "text-foreground text-sm leading-relaxed mb-4", children: [
|
|
140
|
+
'"',
|
|
141
|
+
review.text,
|
|
142
|
+
'"'
|
|
143
|
+
] }),
|
|
144
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
|
|
145
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
146
|
+
/* @__PURE__ */ jsx("div", { className: "h-8 w-8 rounded-full bg-accent flex items-center justify-center text-primary font-semibold text-sm", children: review.author[0] }),
|
|
147
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium text-foreground", children: review.author })
|
|
148
|
+
] }),
|
|
149
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground", children: review.date })
|
|
150
|
+
] })
|
|
151
|
+
] }, review.id)) })
|
|
152
|
+
] }) });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/public/index.tsx
|
|
156
|
+
function createReputationWebsite(options) {
|
|
157
|
+
const basePath = options?.basePath ?? "/reviews";
|
|
158
|
+
const heading = options?.heading ?? "O que os pacientes dizem";
|
|
159
|
+
const provider = options?.dataProvider ?? createSafeDataProvider(
|
|
160
|
+
() => createSupabaseReputationProvider(),
|
|
161
|
+
() => createMockReputationProvider({ seed: options?.seed })
|
|
162
|
+
);
|
|
163
|
+
const value = { provider };
|
|
164
|
+
const Provider = ({ children }) => createElement(ReputationProvider, { value, children });
|
|
165
|
+
Provider.displayName = "ReputationWebsiteProvider";
|
|
166
|
+
const ReviewsScreen = () => createElement(ReviewsList, { heading });
|
|
167
|
+
ReviewsScreen.displayName = "ReviewsScreen";
|
|
168
|
+
const manifest = {
|
|
169
|
+
id: "reputation",
|
|
170
|
+
name: "Reviews",
|
|
171
|
+
icon: "Star",
|
|
172
|
+
version: "0.1.0",
|
|
173
|
+
scope: options?.scope ?? "universal",
|
|
174
|
+
verticalId: options?.verticalId,
|
|
175
|
+
scaffolds: ["website", "landing_page"],
|
|
176
|
+
defaultEnabled: true,
|
|
177
|
+
dependencies: [],
|
|
178
|
+
navigation: [],
|
|
179
|
+
routes: [{ path: basePath, component: ReviewsScreen, guard: "public" }],
|
|
180
|
+
widgets: []
|
|
181
|
+
};
|
|
182
|
+
return { manifest, Provider, dataProvider: provider };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export { ReputationProvider, ReviewsList, createMockReputationProvider, createReputationWebsite, createSupabaseReputationProvider, useReputationContext, useReviewSummary, useReviews };
|
|
186
|
+
//# sourceMappingURL=index.js.map
|
|
187
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/data/mock.ts","../../src/data/supabase.ts","../../src/context.tsx","../../src/hooks/useReviews.ts","../../src/hooks/useReviewSummary.ts","../../src/components/ReviewsList.tsx","../../src/public/index.tsx"],"names":["useState","useEffect","jsx"],"mappings":";;;;;;;;AAaA,IAAM,gBAAA,GAA6B;AAAA,EACjC,EAAE,EAAA,EAAI,IAAA,EAAM,MAAA,EAAQ,WAAA,EAAa,MAAA,EAAQ,QAAA,EAAU,MAAA,EAAQ,CAAA,EAAG,IAAA,EAAM,mCAAA,EAAqC,IAAA,EAAM,UAAA,EAAW;AAAA,EAC1H,EAAE,EAAA,EAAI,IAAA,EAAM,MAAA,EAAQ,QAAA,EAAU,MAAA,EAAQ,UAAA,EAAY,MAAA,EAAQ,CAAA,EAAG,IAAA,EAAM,wCAAA,EAAuC,IAAA,EAAM,UAAA,EAAW;AAAA,EAC3H,EAAE,EAAA,EAAI,IAAA,EAAM,MAAA,EAAQ,UAAA,EAAY,MAAA,EAAQ,QAAA,EAAU,MAAA,EAAQ,CAAA,EAAG,IAAA,EAAM,qBAAA,EAAuB,IAAA,EAAM,UAAA;AAClG,CAAA;AAEA,SAAS,eAAe,OAAA,EAAkC;AACxD,EAAA,MAAM,QAAQ,OAAA,CAAQ,MAAA;AACtB,EAAA,MAAM,UAAU,KAAA,KAAU,CAAA,GAAI,IAAI,IAAA,CAAK,KAAA,CAAO,QAAQ,MAAA,CAAO,CAAC,CAAA,EAAG,CAAA,KAAM,IAAI,CAAA,CAAE,MAAA,EAAQ,CAAC,CAAA,GAAI,KAAA,GAAS,EAAE,CAAA,GAAI,EAAA;AACzG,EAAA,MAAM,YAAA,GAAe,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,GAAA,CAAI,CAAC,KAAA,MAAW;AAAA,IACnD,KAAA;AAAA,IACA,KAAA,EAAO,OAAA,CAAQ,MAAA,CAAO,CAAC,CAAA,KAAM,IAAA,CAAK,KAAA,CAAM,CAAA,CAAE,MAAM,CAAA,KAAM,KAAK,CAAA,CAAE;AAAA,GAC/D,CAAE,CAAA;AACF,EAAA,OAAO,EAAE,OAAA,EAAS,KAAA,EAAO,YAAA,EAAa;AACxC;AAEO,SAAS,6BAA6B,OAAA,EAAiE;AAC5G,EAAA,MAAM,OAAA,GAAoB,OAAA,EAAS,IAAA,EAAM,OAAA,IAAW,gBAAA;AACpD,EAAA,MAAM,OAAA,GAAyB,OAAA,EAAS,IAAA,EAAM,OAAA,IAAW,eAAe,OAAO,CAAA;AAE/E,EAAA,OAAO;AAAA,IACL,MAAM,YAAY,KAAA,EAA4C;AAC5D,MAAA,IAAI,MAAA,GAAS,OAAA;AACb,MAAA,IAAI,KAAA,EAAO,SAAA,IAAa,IAAA,EAAM,MAAA,GAAS,MAAA,CAAO,MAAA,CAAO,CAAC,CAAA,KAAM,CAAA,CAAE,MAAA,IAAU,KAAA,CAAM,SAAU,CAAA;AACxF,MAAA,IAAI,KAAA,EAAO,SAAS,IAAA,EAAM,MAAA,GAAS,OAAO,KAAA,CAAM,CAAA,EAAG,MAAM,KAAK,CAAA;AAC9D,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAAA,IACA,MAAM,UAAA,GAAqC;AACzC,MAAA,OAAO,OAAA;AAAA,IACT;AAAA,GACF;AACF;;;AChCO,SAAS,gCAAA,GAA2D;AACzE,EAAA,MAAM,iBAAiB,MAAa;AAClC,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KAEF;AAAA,EACF,CAAA;AACA,EAAA,OAAO;AAAA,IACL,WAAA,EAAa,CAAC,MAAA,KAAgD,cAAA,EAAe;AAAA,IAC7E,UAAA,EAAY,MAA8B,cAAA;AAAe,GAC3D;AACF;AChBA,IAAM,iBAAA,GAAoB,cAA6C,IAAI,CAAA;AAEpE,SAAS,kBAAA,CAAmB,EAAE,KAAA,EAAO,QAAA,EAAS,EAA2D;AAC9G,EAAA,uBAAO,GAAA,CAAC,iBAAA,CAAkB,QAAA,EAAlB,EAA2B,OAAe,QAAA,EAAS,CAAA;AAC7D;AAEO,SAAS,oBAAA,GAA+C;AAC7D,EAAA,MAAM,GAAA,GAAM,WAAW,iBAAiB,CAAA;AACxC,EAAA,IAAI,CAAC,GAAA,EAAK;AACR,IAAA,MAAM,IAAI,MAAM,oFAAoF,CAAA;AAAA,EACtG;AACA,EAAA,OAAO,GAAA;AACT;ACRO,SAAS,WAAW,KAAA,EAA2C;AACpE,EAAA,MAAM,EAAE,QAAA,EAAS,GAAI,oBAAA,EAAqB;AAC1C,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,QAAA,CAAmB,EAAE,CAAA;AACnD,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,IAAI,CAAA;AAC3C,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAuB,IAAI,CAAA;AAErD,EAAA,MAAM,QAAQ,KAAA,EAAO,KAAA;AACrB,EAAA,MAAM,YAAY,KAAA,EAAO,SAAA;AAEzB,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,MAAA,GAAS,IAAA;AACb,IAAA,UAAA,CAAW,IAAI,CAAA;AACf,IAAA,QAAA,CACG,WAAA,CAAY,EAAE,KAAA,EAAO,SAAA,EAAW,CAAA,CAChC,IAAA,CAAK,CAAC,MAAA,KAAW;AAChB,MAAA,IAAI,CAAC,MAAA,EAAQ;AACb,MAAA,UAAA,CAAW,MAAM,CAAA;AACjB,MAAA,QAAA,CAAS,IAAI,CAAA;AAAA,IACf,CAAC,CAAA,CACA,KAAA,CAAM,CAAC,GAAA,KAAQ;AACd,MAAA,IAAI,CAAC,MAAA,EAAQ;AACb,MAAA,QAAA,CAAS,GAAA,YAAe,QAAQ,GAAA,GAAM,IAAI,MAAM,MAAA,CAAO,GAAG,CAAC,CAAC,CAAA;AAAA,IAC9D,CAAC,CAAA,CACA,OAAA,CAAQ,MAAM;AACb,MAAA,IAAI,MAAA,aAAmB,KAAK,CAAA;AAAA,IAC9B,CAAC,CAAA;AACH,IAAA,OAAO,MAAM;AACX,MAAA,MAAA,GAAS,KAAA;AAAA,IACX,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,QAAA,EAAU,KAAA,EAAO,SAAS,CAAC,CAAA;AAE/B,EAAA,OAAO,EAAE,OAAA,EAAS,OAAA,EAAS,KAAA,EAAM;AACnC;AChCO,SAAS,gBAAA,GAA2C;AACzD,EAAA,MAAM,EAAE,QAAA,EAAS,GAAI,oBAAA,EAAqB;AAC1C,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAIA,SAA+B,IAAI,CAAA;AACjE,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAIA,SAAS,IAAI,CAAA;AAC3C,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAIA,SAAuB,IAAI,CAAA;AAErD,EAAAC,UAAU,MAAM;AACd,IAAA,IAAI,MAAA,GAAS,IAAA;AACb,IAAA,UAAA,CAAW,IAAI,CAAA;AACf,IAAA,QAAA,CACG,UAAA,EAAW,CACX,IAAA,CAAK,CAAC,MAAA,KAAW;AAChB,MAAA,IAAI,CAAC,MAAA,EAAQ;AACb,MAAA,UAAA,CAAW,MAAM,CAAA;AACjB,MAAA,QAAA,CAAS,IAAI,CAAA;AAAA,IACf,CAAC,CAAA,CACA,KAAA,CAAM,CAAC,GAAA,KAAQ;AACd,MAAA,IAAI,CAAC,MAAA,EAAQ;AACb,MAAA,QAAA,CAAS,GAAA,YAAe,QAAQ,GAAA,GAAM,IAAI,MAAM,MAAA,CAAO,GAAG,CAAC,CAAC,CAAA;AAAA,IAC9D,CAAC,CAAA,CACA,OAAA,CAAQ,MAAM;AACb,MAAA,IAAI,MAAA,aAAmB,KAAK,CAAA;AAAA,IAC9B,CAAC,CAAA;AACH,IAAA,OAAO,MAAM;AACX,MAAA,MAAA,GAAS,KAAA;AAAA,IACX,CAAA;AAAA,EACF,CAAA,EAAG,CAAC,QAAQ,CAAC,CAAA;AAEb,EAAA,OAAO,EAAE,OAAA,EAAS,OAAA,EAAS,KAAA,EAAM;AACnC;ACpCA,SAAS,KAAA,CAAM,EAAE,MAAA,EAAO,EAAuB;AAC7C,EAAA,uBACEC,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,6BACZ,QAAA,EAAA,CAAC,CAAA,EAAG,CAAA,EAAG,CAAA,EAAG,GAAG,CAAC,CAAA,CAAE,GAAA,CAAI,CAAC,sBACpBA,GAAAA;AAAA,IAAC,IAAA;AAAA,IAAA;AAAA,MAEC,SAAA,EAAW,CAAA,QAAA,EAAW,CAAA,IAAK,MAAA,GAAS,8BAA8B,0BAA0B,CAAA;AAAA,KAAA;AAAA,IADvF;AAAA,GAGR,CAAA,EACH,CAAA;AAEJ;AAOO,SAAS,WAAA,CAAY,EAAE,KAAA,EAAO,OAAA,EAAQ,EAAyC;AACpF,EAAA,MAAM,EAAE,OAAA,EAAS,OAAA,KAAY,UAAA,CAAW,EAAE,OAAO,CAAA;AACjD,EAAA,MAAM,EAAE,OAAA,EAAQ,GAAI,gBAAA,EAAiB;AAErC,EAAA,uBACEA,IAAC,SAAA,EAAA,EAAQ,SAAA,EAAU,uBACjB,QAAA,kBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,WAAU,wBAAA,EACb,QAAA,EAAA;AAAA,oBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,WAAU,mBAAA,EACZ,QAAA,EAAA;AAAA,MAAA,OAAA,mBACCA,GAAAA,CAAC,IAAA,EAAA,EAAG,SAAA,EAAU,kEAAA,EAAoE,mBAAQ,CAAA,GACxF,IAAA;AAAA,MACH,OAAA,mBACC,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,wCAAA,EACb,QAAA,EAAA;AAAA,wBAAAA,IAAC,KAAA,EAAA,EAAM,MAAA,EAAQ,KAAK,KAAA,CAAM,OAAA,CAAQ,OAAO,CAAA,EAAG,CAAA;AAAA,wBAC5CA,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,uCAAA,EAAyC,kBAAQ,OAAA,EAAQ,CAAA;AAAA,wBACzE,IAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,+BAAA,EAAgC,QAAA,EAAA;AAAA,UAAA,OAAA;AAAA,UAAG,OAAA,CAAQ,KAAA;AAAA,UAAM;AAAA,SAAA,EAAW;AAAA,OAAA,EAC9E,CAAA,GACE;AAAA,KAAA,EACN,CAAA;AAAA,IAEC,OAAA,mBACCA,GAAAA,CAAC,GAAA,EAAA,EAAE,WAAU,mCAAA,EAAoC,QAAA,EAAA,mCAAA,EAAsB,oBAEvEA,GAAAA,CAAC,SAAI,SAAA,EAAU,uCAAA,EACZ,kBAAQ,GAAA,CAAI,CAAC,2BACZ,IAAA,CAAC,KAAA,EAAA,EAAoB,WAAU,wDAAA,EAC7B,QAAA,EAAA;AAAA,sBAAAA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,MAAA,EACb,QAAA,kBAAAA,IAAC,KAAA,EAAA,EAAM,MAAA,EAAQ,MAAA,CAAO,MAAA,EAAQ,CAAA,EAChC,CAAA;AAAA,sBACA,IAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAU,8CAAA,EAA+C,QAAA,EAAA;AAAA,QAAA,GAAA;AAAA,QAAE,MAAA,CAAO,IAAA;AAAA,QAAK;AAAA,OAAA,EAAC,CAAA;AAAA,sBAC3E,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,mCAAA,EACb,QAAA,EAAA;AAAA,wBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,WAAU,yBAAA,EACb,QAAA,EAAA;AAAA,0BAAAA,IAAC,KAAA,EAAA,EAAI,SAAA,EAAU,sGACZ,QAAA,EAAA,MAAA,CAAO,MAAA,CAAO,CAAC,CAAA,EAClB,CAAA;AAAA,0BACAA,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,qCAAA,EAAuC,iBAAO,MAAA,EAAO;AAAA,SAAA,EACvE,CAAA;AAAA,wBACAA,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,+BAAA,EAAiC,iBAAO,IAAA,EAAK;AAAA,OAAA,EAC/D;AAAA,KAAA,EAAA,EAbQ,MAAA,CAAO,EAcjB,CACD,CAAA,EACH;AAAA,GAAA,EAEJ,CAAA,EACF,CAAA;AAEJ;;;AC/BO,SAAS,wBAAwB,OAAA,EAA6D;AACnG,EAAA,MAAM,QAAA,GAAW,SAAS,QAAA,IAAY,UAAA;AACtC,EAAA,MAAM,OAAA,GAAU,SAAS,OAAA,IAAW,0BAAA;AACpC,EAAA,MAAM,QAAA,GACJ,SAAS,YAAA,IACT,sBAAA;AAAA,IACE,MAAM,gCAAA,EAAiC;AAAA,IACvC,MAAM,4BAAA,CAA6B,EAAE,IAAA,EAAM,OAAA,EAAS,MAAM;AAAA,GAC5D;AAEF,EAAA,MAAM,KAAA,GAAgC,EAAE,QAAA,EAAS;AACjD,EAAA,MAAM,QAAA,GAAwC,CAAC,EAAE,QAAA,EAAS,KACxD,cAAc,kBAAA,EAAoB,EAAE,KAAA,EAAO,QAAA,EAAU,CAAA;AACvD,EAAA,QAAA,CAAS,WAAA,GAAc,2BAAA;AAEvB,EAAA,MAAM,gBAA6B,MAAM,aAAA,CAAc,WAAA,EAAa,EAAE,SAAS,CAAA;AAC/E,EAAA,aAAA,CAAc,WAAA,GAAc,eAAA;AAE5B,EAAA,MAAM,QAAA,GAA2B;AAAA,IAC/B,EAAA,EAAI,YAAA;AAAA,IACJ,IAAA,EAAM,SAAA;AAAA,IACN,IAAA,EAAM,MAAA;AAAA,IACN,OAAA,EAAS,OAAA;AAAA,IACT,KAAA,EAAO,SAAS,KAAA,IAAS,WAAA;AAAA,IACzB,YAAY,OAAA,EAAS,UAAA;AAAA,IACrB,SAAA,EAAW,CAAC,SAAA,EAAW,cAAc,CAAA;AAAA,IACrC,cAAA,EAAgB,IAAA;AAAA,IAChB,cAAc,EAAC;AAAA,IACf,YAAY,EAAC;AAAA,IACb,MAAA,EAAQ,CAAC,EAAE,IAAA,EAAM,UAAU,SAAA,EAAW,aAAA,EAAe,KAAA,EAAO,QAAA,EAAU,CAAA;AAAA,IACtE,SAAS;AAAC,GACZ;AAEA,EAAA,OAAO,EAAE,QAAA,EAAU,QAAA,EAAU,YAAA,EAAc,QAAA,EAAS;AACtD","file":"index.js","sourcesContent":["import type { ReputationDataProvider } from './types'\nimport type { Review, ReviewSummary, ReviewListQuery } from '../types'\n\nexport interface ReputationSeed {\n reviews: Review[]\n /** Explicit aggregate. If omitted, it is computed from `reviews`. */\n summary?: ReviewSummary\n}\n\nexport interface MockReputationProviderOptions {\n seed?: ReputationSeed\n}\n\nconst FALLBACK_REVIEWS: Review[] = [\n { id: 'r1', author: 'Camila R.', source: 'Google', rating: 5, text: 'Excelente atendimento, recomendo!', date: 'Jun 2025' },\n { id: 'r2', author: 'Tom B.', source: 'Facebook', rating: 5, text: 'Processo simples e resultado ótimo.', date: 'Jun 2025' },\n { id: 'r3', author: 'Aisha K.', source: 'Google', rating: 4, text: 'Muito bom no geral.', date: 'Jun 2025' },\n]\n\nfunction computeSummary(reviews: Review[]): ReviewSummary {\n const count = reviews.length\n const average = count === 0 ? 0 : Math.round((reviews.reduce((s, r) => s + r.rating, 0) / count) * 10) / 10\n const distribution = [5, 4, 3, 2, 1].map((stars) => ({\n stars,\n count: reviews.filter((r) => Math.round(r.rating) === stars).length,\n }))\n return { average, count, distribution }\n}\n\nexport function createMockReputationProvider(options?: MockReputationProviderOptions): ReputationDataProvider {\n const reviews: Review[] = options?.seed?.reviews ?? FALLBACK_REVIEWS\n const summary: ReviewSummary = options?.seed?.summary ?? computeSummary(reviews)\n\n return {\n async listReviews(query?: ReviewListQuery): Promise<Review[]> {\n let result = reviews\n if (query?.minRating != null) result = result.filter((r) => r.rating >= query.minRating!)\n if (query?.limit != null) result = result.slice(0, query.limit)\n return result\n },\n async getSummary(): Promise<ReviewSummary> {\n return summary\n },\n }\n}\n","import type { ReputationDataProvider } from './types'\nimport type { Review, ReviewSummary, ReviewListQuery } from '../types'\n\n// ---------------------------------------------------------------------------\n// Supabase-backed reputation provider — STUB (deferred to Phase 2).\n//\n// Later: read a `reviews` table (tenant-scoped, RLS) and/or synced Google/\n// Facebook reviews. Swapping this in is a pure provider change — hooks and\n// components are untouched. Throws until then so createSafeDataProvider falls\n// back to the mock/seed provider whenever no Supabase client is configured.\n// ---------------------------------------------------------------------------\n\nexport function createSupabaseReputationProvider(): ReputationDataProvider {\n const notImplemented = (): never => {\n throw new Error(\n '[plugin-reputation] Supabase provider not implemented yet — deferred to Phase 2. ' +\n 'Run on the mock/seed provider (no Supabase client configured) for now.',\n )\n }\n return {\n listReviews: (_query?: ReviewListQuery): Promise<Review[]> => notImplemented(),\n getSummary: (): Promise<ReviewSummary> => notImplemented(),\n }\n}\n","import { createContext, useContext, type ReactNode } from 'react'\nimport type { ReputationDataProvider } from './data/types'\n\nexport interface ReputationContextValue {\n provider: ReputationDataProvider\n}\n\nconst ReputationContext = createContext<ReputationContextValue | null>(null)\n\nexport function ReputationProvider({ value, children }: { value: ReputationContextValue; children: ReactNode }) {\n return <ReputationContext.Provider value={value}>{children}</ReputationContext.Provider>\n}\n\nexport function useReputationContext(): ReputationContextValue {\n const ctx = useContext(ReputationContext)\n if (!ctx) {\n throw new Error('[plugin-reputation] useReputationContext must be used within <ReputationProvider>.')\n }\n return ctx\n}\n","import { useEffect, useState } from 'react'\nimport { useReputationContext } from '../context'\nimport type { Review, ReviewListQuery } from '../types'\n\nexport interface UseReviewsResult {\n reviews: Review[]\n loading: boolean\n error: Error | null\n}\n\n/** Fetch the review list from the active reputation provider. */\nexport function useReviews(query?: ReviewListQuery): UseReviewsResult {\n const { provider } = useReputationContext()\n const [reviews, setReviews] = useState<Review[]>([])\n const [loading, setLoading] = useState(true)\n const [error, setError] = useState<Error | null>(null)\n\n const limit = query?.limit\n const minRating = query?.minRating\n\n useEffect(() => {\n let active = true\n setLoading(true)\n provider\n .listReviews({ limit, minRating })\n .then((result) => {\n if (!active) return\n setReviews(result)\n setError(null)\n })\n .catch((err) => {\n if (!active) return\n setError(err instanceof Error ? err : new Error(String(err)))\n })\n .finally(() => {\n if (active) setLoading(false)\n })\n return () => {\n active = false\n }\n }, [provider, limit, minRating])\n\n return { reviews, loading, error }\n}\n","import { useEffect, useState } from 'react'\nimport { useReputationContext } from '../context'\nimport type { ReviewSummary } from '../types'\n\nexport interface UseReviewSummaryResult {\n summary: ReviewSummary | null\n loading: boolean\n error: Error | null\n}\n\n/** Fetch the aggregate rating summary (average + count + distribution). */\nexport function useReviewSummary(): UseReviewSummaryResult {\n const { provider } = useReputationContext()\n const [summary, setSummary] = useState<ReviewSummary | null>(null)\n const [loading, setLoading] = useState(true)\n const [error, setError] = useState<Error | null>(null)\n\n useEffect(() => {\n let active = true\n setLoading(true)\n provider\n .getSummary()\n .then((result) => {\n if (!active) return\n setSummary(result)\n setError(null)\n })\n .catch((err) => {\n if (!active) return\n setError(err instanceof Error ? err : new Error(String(err)))\n })\n .finally(() => {\n if (active) setLoading(false)\n })\n return () => {\n active = false\n }\n }, [provider])\n\n return { summary, loading, error }\n}\n","import { Star } from 'lucide-react'\nimport { useReviews } from '../hooks/useReviews'\nimport { useReviewSummary } from '../hooks/useReviewSummary'\n\nfunction Stars({ rating }: { rating: number }) {\n return (\n <div className=\"flex items-center gap-0.5\">\n {[1, 2, 3, 4, 5].map((i) => (\n <Star\n key={i}\n className={`h-4 w-4 ${i <= rating ? 'fill-primary text-primary' : 'text-muted-foreground/30'}`}\n />\n ))}\n </div>\n )\n}\n\n/**\n * Default reviews list with an aggregate header — token-only so it inherits the\n * host theme. Optional: a host with bespoke review cards can ignore this and map\n * `useReviews()` / `useReviewSummary()` itself (as hempdent does on its home).\n */\nexport function ReviewsList({ limit, heading }: { limit?: number; heading?: string }) {\n const { reviews, loading } = useReviews({ limit })\n const { summary } = useReviewSummary()\n\n return (\n <section className=\"py-16 bg-background\">\n <div className=\"container mx-auto px-6\">\n <div className=\"text-center mb-12\">\n {heading ? (\n <h1 className=\"font-heading text-4xl md:text-5xl font-bold text-foreground mb-3\">{heading}</h1>\n ) : null}\n {summary ? (\n <div className=\"flex items-center justify-center gap-2\">\n <Stars rating={Math.round(summary.average)} />\n <span className=\"text-foreground font-semibold text-lg\">{summary.average}</span>\n <span className=\"text-muted-foreground text-sm\">· {summary.count} avaliações</span>\n </div>\n ) : null}\n </div>\n\n {loading ? (\n <p className=\"text-center text-muted-foreground\">Carregando avaliações…</p>\n ) : (\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-6\">\n {reviews.map((review) => (\n <div key={review.id} className=\"rounded-2xl bg-card border border-border p-6 shadow-sm\">\n <div className=\"mb-3\">\n <Stars rating={review.rating} />\n </div>\n <p className=\"text-foreground text-sm leading-relaxed mb-4\">\"{review.text}\"</p>\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <div className=\"h-8 w-8 rounded-full bg-accent flex items-center justify-center text-primary font-semibold text-sm\">\n {review.author[0]}\n </div>\n <span className=\"text-sm font-medium text-foreground\">{review.author}</span>\n </div>\n <span className=\"text-xs text-muted-foreground\">{review.date}</span>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </section>\n )\n}\n","import { createElement, type FC, type ReactNode } from 'react'\nimport type { PluginManifest, PluginScope, VerticalId } from '@fayz-ai/core'\nimport { createSafeDataProvider } from '@fayz-ai/core'\nimport { createMockReputationProvider, type ReputationSeed } from '../data/mock'\nimport { createSupabaseReputationProvider } from '../data/supabase'\nimport { ReputationProvider, type ReputationContextValue } from '../context'\nimport { ReviewsList } from '../components/ReviewsList'\nimport type { ReputationDataProvider } from '../data/types'\n\n// ---------------------------------------------------------------------------\n// @fayz-ai/plugin-reputation/public — website reviews surface.\n//\n// Lean entry (no admin ReputationHome / @fayz-ai/ui). Returns a { manifest,\n// Provider } bundle the host reads uniformly: the Provider wraps the app root so\n// useReviews()/useReviewSummary() power the host's own review markup, and\n// manifest.routes ships an optional /reviews page.\n// ---------------------------------------------------------------------------\n\nexport interface ReputationWebsiteOptions {\n /** Base path for the optional public \"all reviews\" page. Default '/reviews'. */\n basePath?: string\n /** Seed reviews + summary for the mock provider (used until a real backend is wired). */\n seed?: ReputationSeed\n /** Inject a custom provider. Overrides the safe mock/Supabase resolver. */\n dataProvider?: ReputationDataProvider\n /** Heading for the optional /reviews screen. */\n heading?: string\n scope?: PluginScope\n verticalId?: VerticalId\n}\n\nexport interface ReputationWebsitePlugin {\n manifest: PluginManifest\n Provider: FC<{ children: ReactNode }>\n dataProvider: ReputationDataProvider\n}\n\nexport function createReputationWebsite(options?: ReputationWebsiteOptions): ReputationWebsitePlugin {\n const basePath = options?.basePath ?? '/reviews'\n const heading = options?.heading ?? 'O que os pacientes dizem'\n const provider =\n options?.dataProvider ??\n createSafeDataProvider(\n () => createSupabaseReputationProvider(),\n () => createMockReputationProvider({ seed: options?.seed }),\n )\n\n const value: ReputationContextValue = { provider }\n const Provider: FC<{ children: ReactNode }> = ({ children }) =>\n createElement(ReputationProvider, { value, children })\n Provider.displayName = 'ReputationWebsiteProvider'\n\n const ReviewsScreen: FC<unknown> = () => createElement(ReviewsList, { heading })\n ReviewsScreen.displayName = 'ReviewsScreen'\n\n const manifest: PluginManifest = {\n id: 'reputation',\n name: 'Reviews',\n icon: 'Star',\n version: '0.1.0',\n scope: options?.scope ?? 'universal',\n verticalId: options?.verticalId,\n scaffolds: ['website', 'landing_page'],\n defaultEnabled: true,\n dependencies: [],\n navigation: [],\n routes: [{ path: basePath, component: ReviewsScreen, guard: 'public' }],\n widgets: [],\n }\n\n return { manifest, Provider, dataProvider: provider }\n}\n\n// Public API — website surface\nexport type { Review, ReviewSummary, ReviewSource, ReviewListQuery } from '../types'\nexport type { ReputationDataProvider } from '../data/types'\nexport { createMockReputationProvider, createSupabaseReputationProvider } from '../data'\nexport type { ReputationSeed } from '../data/mock'\nexport { ReputationProvider, useReputationContext } from '../context'\nexport type { ReputationContextValue } from '../context'\nexport { useReviews, useReviewSummary } from '../hooks'\nexport { ReviewsList } from '../components/ReviewsList'\n"]}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type ReviewSource = 'Google' | 'Facebook' | 'Instagram' | 'Website' | (string & {});
|
|
2
|
+
export interface Review {
|
|
3
|
+
id: string;
|
|
4
|
+
/** Reviewer display name. */
|
|
5
|
+
author: string;
|
|
6
|
+
rating: number;
|
|
7
|
+
text: string;
|
|
8
|
+
/** Human-readable date (e.g. "Janeiro 2025"). */
|
|
9
|
+
date: string;
|
|
10
|
+
source?: ReviewSource;
|
|
11
|
+
/** Whether the business has replied (admin surface). */
|
|
12
|
+
replied?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface ReviewSummary {
|
|
15
|
+
/** Average rating, e.g. 4.8. */
|
|
16
|
+
average: number;
|
|
17
|
+
/** Total number of ratings, e.g. 123. */
|
|
18
|
+
count: number;
|
|
19
|
+
/** Optional star distribution (5→1). */
|
|
20
|
+
distribution?: Array<{
|
|
21
|
+
stars: number;
|
|
22
|
+
count: number;
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
export interface ReviewListQuery {
|
|
26
|
+
limit?: number;
|
|
27
|
+
/** Minimum rating to include. */
|
|
28
|
+
minRating?: number;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAKA,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,GAAG,SAAS,GAAG,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAE1F,MAAM,WAAW,MAAM;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,6BAA6B;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,CAAC,EAAE,YAAY,CAAA;IACrB,wDAAwD;IACxD,OAAO,CAAC,EAAE,OAAO,CAAA;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAA;IACf,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,wCAAwC;IACxC,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACvD;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iCAAiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fayz-ai/plugin-reputation",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Fayz SDK — reviews & reputation management plugin",
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"description": "[experimental] Fayz SDK — reviews & reputation management plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -12,6 +12,12 @@
|
|
|
12
12
|
"types": "./dist/index.d.ts",
|
|
13
13
|
"import": "./dist/index.js",
|
|
14
14
|
"require": "./dist/index.cjs"
|
|
15
|
+
},
|
|
16
|
+
"./public": {
|
|
17
|
+
"source": "./src/public/index.tsx",
|
|
18
|
+
"types": "./dist/public/index.d.ts",
|
|
19
|
+
"import": "./dist/public/index.js",
|
|
20
|
+
"require": "./dist/public/index.cjs"
|
|
15
21
|
}
|
|
16
22
|
},
|
|
17
23
|
"files": [
|
|
@@ -24,8 +30,8 @@
|
|
|
24
30
|
},
|
|
25
31
|
"dependencies": {
|
|
26
32
|
"lucide-react": "^0.400.0",
|
|
27
|
-
"@fayz-ai/core": "^0.
|
|
28
|
-
"@fayz-ai/ui": "^0.
|
|
33
|
+
"@fayz-ai/core": "^0.7.1",
|
|
34
|
+
"@fayz-ai/ui": "^0.7.0"
|
|
29
35
|
},
|
|
30
36
|
"devDependencies": {
|
|
31
37
|
"@types/react": "^18.3.0",
|
|
@@ -40,9 +46,6 @@
|
|
|
40
46
|
"fayz-plugin",
|
|
41
47
|
"fayz-sdk"
|
|
42
48
|
],
|
|
43
|
-
"publishConfig": {
|
|
44
|
-
"access": "public"
|
|
45
|
-
},
|
|
46
49
|
"scripts": {
|
|
47
50
|
"build": "tsup && tsc --emitDeclarationOnly --declaration --declarationMap --noEmit false",
|
|
48
51
|
"dev": "tsup --watch",
|
|
@@ -0,0 +1,69 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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/mock.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
CHANGED
|
@@ -6,6 +6,10 @@ import { ReputationHome } from './views/ReputationHome'
|
|
|
6
6
|
// @fayz-ai/plugin-reputation — reviews & reputation (GoHighLevel "Reputation").
|
|
7
7
|
// Universal plugin. M1 ships a rich mock home; Google/Facebook review sync +
|
|
8
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.
|
|
9
13
|
// ---------------------------------------------------------------------------
|
|
10
14
|
|
|
11
15
|
export interface ReputationPluginOptions {
|