@fayz-ai/plugin-reputation 0.9.4 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/dist/chunk-BGCEG4NA.js +410 -0
- package/dist/chunk-BGCEG4NA.js.map +1 -0
- package/dist/components/ProductRating.d.ts +15 -0
- package/dist/components/ProductRating.d.ts.map +1 -0
- package/dist/components/ProductReviews.d.ts +12 -0
- package/dist/components/ProductReviews.d.ts.map +1 -0
- package/dist/components/Stars.d.ts +15 -0
- package/dist/components/Stars.d.ts.map +1 -0
- package/dist/components/TopReviews.d.ts +13 -0
- package/dist/components/TopReviews.d.ts.map +1 -0
- package/dist/data/index.d.ts +4 -1
- package/dist/data/index.d.ts.map +1 -1
- package/dist/data/mock.d.ts +2 -0
- package/dist/data/mock.d.ts.map +1 -1
- package/dist/data/registries.d.ts +3 -0
- package/dist/data/registries.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +5 -1
- package/dist/data/supabase.d.ts.map +1 -1
- package/dist/data/tables.d.ts +8 -0
- package/dist/data/tables.d.ts.map +1 -0
- package/dist/data/types.d.ts +24 -3
- package/dist/data/types.d.ts.map +1 -1
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useManagedReviews.d.ts +23 -0
- package/dist/hooks/useManagedReviews.d.ts.map +1 -0
- package/dist/hooks/useProductReviews.d.ts +23 -0
- package/dist/hooks/useProductReviews.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +238 -48
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.d.ts +6 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/public/index.d.ts +14 -2
- package/dist/public/index.d.ts.map +1 -1
- package/dist/public/index.js +295 -84
- package/dist/public/index.js.map +1 -1
- package/dist/types.d.ts +57 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/views/ReputationHome.d.ts.map +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @fayz-ai/plugin-reputation
|
|
2
2
|
|
|
3
|
-
> **Status: experimental (incubating).**
|
|
3
|
+
> **Status: experimental (incubating).** Now carries most of the capability bar — a mock/Supabase provider pair, an entity registry, migrations with canonical RLS, seed data and tests. Still short of `settings`, and the Google/Facebook sync remains unbuilt. Fine to explore in dogfoods; its API may still change without notice.
|
|
4
4
|
|
|
5
5
|
> Reviews and reputation, in one place — see the stars, reply, ask for more.
|
|
6
6
|
|
|
@@ -9,14 +9,39 @@
|
|
|
9
9
|
|
|
10
10
|
Reputation is the cheapest growth lever a local business has, and the easiest to ignore. Reviews sit scattered across Google and Facebook, replies never happen, and nobody ever asks the happy customer for a rating. `plugin-reputation` is the GoHighLevel-style reputation surface for the Fayz engine: a single home for your rating, your review feed, and the actions that move it.
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
It now does two jobs. The **backoffice** is a moderation queue: an average-rating card, a star distribution, and a review feed where each row can be published, refused or deleted — reading a real table, not a fixture. The **storefront surface** (`./public`) reads the approved rows and renders them: a star rating for a product page, the review list beneath it, and a homepage social-proof section fed by real reviews rather than hardcoded testimonials.
|
|
13
|
+
|
|
14
|
+
Two shapes of review share one table. A **business review** has no product and is what the Google/Facebook feed will fill; a **product review** points at a product and is what a shopper reads on a PDP. Only `approved` rows ever leave the building — the storefront reads column-whitelisted views, never the base table.
|
|
13
15
|
|
|
14
16
|
## What's inside
|
|
15
|
-
- A `/reputation` home: average-rating card, star-distribution bars, and a
|
|
16
|
-
-
|
|
17
|
-
-
|
|
17
|
+
- A `/reputation` home: average-rating card, star-distribution bars, and a moderation queue (publish / refuse / delete)
|
|
18
|
+
- Product reviews: per-product aggregate, per-product feed, and a "verified purchase" badge for reviews tied to an order
|
|
19
|
+
- Storefront components — `ProductRating`, `ProductReviews`, `TopReviews`, `Stars` — in the lean `./public` subpath
|
|
20
|
+
- A migration with canonical tenant RLS plus two anon-readable views (`v_public_product_reviews`, `v_public_product_ratings`)
|
|
21
|
+
- Google / Facebook review sources represented in the UI (sync itself still to come)
|
|
18
22
|
- Configurable navigation (label, position, section) and vertical scoping
|
|
19
23
|
|
|
24
|
+
## On a storefront
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { createReputationWebsite, ProductRating, ProductReviews, TopReviews } from '@fayz-ai/plugin-reputation/public'
|
|
28
|
+
|
|
29
|
+
// Anon storefronts have no session, so the tenant is explicit.
|
|
30
|
+
const reviews = createReputationWebsite({ tenantId: storeId })
|
|
31
|
+
|
|
32
|
+
// Wrap the app in reviews.Provider, then drop components into storefront slots:
|
|
33
|
+
<ProductRating productId={product.id} onClick={scrollToReviews} /> // productDetail/afterTitle
|
|
34
|
+
<ProductReviews productId={product.id} /> // productDetail/afterDetail
|
|
35
|
+
<TopReviews limit={3} productHref={(r) => `/product/${r.productSlug}`} />
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A catalogue grid should not fire one query per card — ask for every rating at once:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const ratings = await reviews.dataProvider.getProductRatings(products.map((p) => p.id))
|
|
42
|
+
<ProductRating average={ratings[p.id].average} count={ratings[p.id].count} />
|
|
43
|
+
```
|
|
44
|
+
|
|
20
45
|
## Install
|
|
21
46
|
```bash
|
|
22
47
|
npm install @fayz-ai/plugin-reputation
|
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core';
|
|
4
|
+
|
|
5
|
+
// src/context.tsx
|
|
6
|
+
var ReputationContext = createContext(null);
|
|
7
|
+
function ReputationProvider({ value, children }) {
|
|
8
|
+
return /* @__PURE__ */ jsx(ReputationContext.Provider, { value, children });
|
|
9
|
+
}
|
|
10
|
+
function useReputationContext() {
|
|
11
|
+
const ctx = useContext(ReputationContext);
|
|
12
|
+
if (!ctx) {
|
|
13
|
+
throw new Error("[plugin-reputation] useReputationContext must be used within <ReputationProvider>.");
|
|
14
|
+
}
|
|
15
|
+
return ctx;
|
|
16
|
+
}
|
|
17
|
+
function useReviewSummary() {
|
|
18
|
+
const { provider } = useReputationContext();
|
|
19
|
+
const [summary, setSummary] = useState(null);
|
|
20
|
+
const [loading, setLoading] = useState(true);
|
|
21
|
+
const [error, setError] = useState(null);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
let active = true;
|
|
24
|
+
setLoading(true);
|
|
25
|
+
provider.getSummary().then((result) => {
|
|
26
|
+
if (!active) return;
|
|
27
|
+
setSummary(result);
|
|
28
|
+
setError(null);
|
|
29
|
+
}).catch((err) => {
|
|
30
|
+
if (!active) return;
|
|
31
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
32
|
+
}).finally(() => {
|
|
33
|
+
if (active) setLoading(false);
|
|
34
|
+
});
|
|
35
|
+
return () => {
|
|
36
|
+
active = false;
|
|
37
|
+
};
|
|
38
|
+
}, [provider]);
|
|
39
|
+
return { summary, loading, error };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/data/mock.ts
|
|
43
|
+
var SEED_REVIEWS = [
|
|
44
|
+
{ id: "r1", author: "Camila R.", source: "Google", rating: 5, text: "Excelente atendimento, recomendo!", date: "Jun 2025" },
|
|
45
|
+
{ id: "r2", author: "Tom B.", source: "Facebook", rating: 5, text: "Processo simples e resultado \xF3timo.", date: "Jun 2025" },
|
|
46
|
+
{ id: "r3", author: "Aisha K.", source: "Google", rating: 4, text: "Muito bom no geral.", date: "Jun 2025" }
|
|
47
|
+
];
|
|
48
|
+
function computeSummary(reviews) {
|
|
49
|
+
const count = reviews.length;
|
|
50
|
+
const average = count === 0 ? 0 : Math.round(reviews.reduce((s, r) => s + r.rating, 0) / count * 10) / 10;
|
|
51
|
+
const distribution = [5, 4, 3, 2, 1].map((stars) => ({
|
|
52
|
+
stars,
|
|
53
|
+
count: reviews.filter((r) => Math.round(r.rating) === stars).length
|
|
54
|
+
}));
|
|
55
|
+
return { average, count, distribution };
|
|
56
|
+
}
|
|
57
|
+
function byRecent(a, b) {
|
|
58
|
+
if (!a.publishedAt || !b.publishedAt) return 0;
|
|
59
|
+
return b.publishedAt.localeCompare(a.publishedAt);
|
|
60
|
+
}
|
|
61
|
+
function applyListQuery(reviews, query) {
|
|
62
|
+
let result = [...reviews];
|
|
63
|
+
if (query?.minRating != null) result = result.filter((r) => r.rating >= query.minRating);
|
|
64
|
+
if (query?.limit != null) result = result.slice(0, query.limit);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
var mockIdCounter = 0;
|
|
68
|
+
function createMockReputationProvider(options) {
|
|
69
|
+
const reviews = [...options?.seed?.reviews ?? SEED_REVIEWS];
|
|
70
|
+
const summary = options?.seed?.summary ?? computeSummary(reviews);
|
|
71
|
+
const forProduct = (productId) => reviews.filter((r) => r.productId === productId);
|
|
72
|
+
return {
|
|
73
|
+
async listReviews(query) {
|
|
74
|
+
return applyListQuery(reviews, query);
|
|
75
|
+
},
|
|
76
|
+
async getSummary() {
|
|
77
|
+
return summary;
|
|
78
|
+
},
|
|
79
|
+
async getProductRating(productId) {
|
|
80
|
+
return computeSummary(forProduct(productId));
|
|
81
|
+
},
|
|
82
|
+
async getProductRatings(productIds) {
|
|
83
|
+
const out = {};
|
|
84
|
+
for (const id of productIds) out[id] = computeSummary(forProduct(id));
|
|
85
|
+
return out;
|
|
86
|
+
},
|
|
87
|
+
async listProductReviews(query) {
|
|
88
|
+
const sorted = forProduct(query.productId).sort(
|
|
89
|
+
query.sort === "rating" ? (a, b) => b.rating - a.rating : byRecent
|
|
90
|
+
);
|
|
91
|
+
return applyListQuery(sorted, query);
|
|
92
|
+
},
|
|
93
|
+
async listTopReviews(query) {
|
|
94
|
+
const productOnly = query?.productOnly ?? true;
|
|
95
|
+
let pool = reviews.filter((r) => productOnly ? r.productId != null : true);
|
|
96
|
+
if (query?.productIds?.length) {
|
|
97
|
+
const wanted = new Set(query.productIds);
|
|
98
|
+
pool = pool.filter((r) => r.productId != null && wanted.has(r.productId));
|
|
99
|
+
}
|
|
100
|
+
pool = pool.sort((a, b) => b.rating - a.rating || byRecent(a, b));
|
|
101
|
+
return applyListQuery(pool, { ...query, limit: query?.limit ?? 6 });
|
|
102
|
+
},
|
|
103
|
+
async listManagedReviews(query) {
|
|
104
|
+
let pool = reviews;
|
|
105
|
+
if (query?.status) pool = pool.filter((r) => (r.status ?? "approved") === query.status);
|
|
106
|
+
if (query?.productId) pool = pool.filter((r) => r.productId === query.productId);
|
|
107
|
+
if (query?.kind === "product") pool = pool.filter((r) => r.productId != null);
|
|
108
|
+
if (query?.kind === "business") pool = pool.filter((r) => r.productId == null);
|
|
109
|
+
return applyListQuery([...pool].sort(byRecent), query);
|
|
110
|
+
},
|
|
111
|
+
async createReview(input) {
|
|
112
|
+
const review = {
|
|
113
|
+
id: `mock-${++mockIdCounter}`,
|
|
114
|
+
author: input.author,
|
|
115
|
+
rating: input.rating,
|
|
116
|
+
text: input.text ?? "",
|
|
117
|
+
date: (/* @__PURE__ */ new Date()).toLocaleDateString("pt-BR", { month: "long", year: "numeric" }),
|
|
118
|
+
publishedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
119
|
+
source: input.source ?? "Website",
|
|
120
|
+
status: input.status ?? "pending",
|
|
121
|
+
productId: input.productId ?? void 0,
|
|
122
|
+
productName: input.productName ?? void 0,
|
|
123
|
+
productSlug: input.productSlug ?? void 0,
|
|
124
|
+
title: input.title ?? void 0,
|
|
125
|
+
verified: input.verified ?? false,
|
|
126
|
+
avatarUrl: input.avatarUrl ?? void 0,
|
|
127
|
+
replyText: input.replyText ?? void 0,
|
|
128
|
+
replied: Boolean(input.replyText)
|
|
129
|
+
};
|
|
130
|
+
reviews.unshift(review);
|
|
131
|
+
return review;
|
|
132
|
+
},
|
|
133
|
+
async updateReview(id, patch) {
|
|
134
|
+
const i = reviews.findIndex((r) => r.id === id);
|
|
135
|
+
if (i < 0) throw new Error(`[plugin-reputation] no review ${id}`);
|
|
136
|
+
const next = {
|
|
137
|
+
...reviews[i],
|
|
138
|
+
...patch.author !== void 0 ? { author: patch.author } : {},
|
|
139
|
+
...patch.rating !== void 0 ? { rating: patch.rating } : {},
|
|
140
|
+
...patch.text !== void 0 ? { text: patch.text } : {},
|
|
141
|
+
...patch.title !== void 0 ? { title: patch.title ?? void 0 } : {},
|
|
142
|
+
...patch.status !== void 0 ? { status: patch.status } : {},
|
|
143
|
+
...patch.verified !== void 0 ? { verified: patch.verified } : {},
|
|
144
|
+
...patch.replyText !== void 0 ? { replyText: patch.replyText ?? void 0, replied: Boolean(patch.replyText) } : {}
|
|
145
|
+
};
|
|
146
|
+
reviews[i] = next;
|
|
147
|
+
return next;
|
|
148
|
+
},
|
|
149
|
+
async setReviewStatus(id, status) {
|
|
150
|
+
const i = reviews.findIndex((r) => r.id === id);
|
|
151
|
+
if (i < 0) throw new Error(`[plugin-reputation] no review ${id}`);
|
|
152
|
+
const next = { ...reviews[i], status };
|
|
153
|
+
reviews[i] = next;
|
|
154
|
+
return next;
|
|
155
|
+
},
|
|
156
|
+
async deleteReview(id) {
|
|
157
|
+
const i = reviews.findIndex((r) => r.id === id);
|
|
158
|
+
if (i >= 0) reviews.splice(i, 1);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
var REVIEWS_VIEW = "v_public_product_reviews";
|
|
163
|
+
var RATINGS_VIEW = "v_public_product_ratings";
|
|
164
|
+
var TABLE = "plg_reputation_reviews";
|
|
165
|
+
function getClient() {
|
|
166
|
+
const supabase = getSupabaseClientOptional();
|
|
167
|
+
if (!supabase) throw new Error("[plugin-reputation] Supabase client not initialized");
|
|
168
|
+
return supabase;
|
|
169
|
+
}
|
|
170
|
+
function resolveTenant(options) {
|
|
171
|
+
const tenantId = options?.tenantId ?? getActiveTenantId();
|
|
172
|
+
if (!tenantId) throw new Error("[plugin-reputation] No tenant to read reviews for");
|
|
173
|
+
return tenantId;
|
|
174
|
+
}
|
|
175
|
+
function humanDate(iso) {
|
|
176
|
+
if (!iso) return "";
|
|
177
|
+
const d = new Date(iso);
|
|
178
|
+
if (Number.isNaN(d.getTime())) return "";
|
|
179
|
+
const label = d.toLocaleDateString("pt-BR", { month: "long", year: "numeric" });
|
|
180
|
+
return label.charAt(0).toUpperCase() + label.slice(1);
|
|
181
|
+
}
|
|
182
|
+
function toReview(row) {
|
|
183
|
+
return {
|
|
184
|
+
id: String(row.id),
|
|
185
|
+
author: row.author_name ?? "Cliente",
|
|
186
|
+
// PostgREST sends numeric as a JSON number, but a proxy that stringifies it
|
|
187
|
+
// would otherwise turn every average into NaN downstream.
|
|
188
|
+
rating: Number(row.rating) || 0,
|
|
189
|
+
text: row.body ?? "",
|
|
190
|
+
date: humanDate(row.published_at),
|
|
191
|
+
source: row.source ?? void 0,
|
|
192
|
+
productId: row.product_id ?? void 0,
|
|
193
|
+
productName: row.product_name ?? void 0,
|
|
194
|
+
productSlug: row.product_slug ?? void 0,
|
|
195
|
+
title: row.title ?? void 0,
|
|
196
|
+
verified: row.verified === true,
|
|
197
|
+
avatarUrl: row.author_avatar_url ?? void 0,
|
|
198
|
+
replyText: row.reply_text ?? void 0,
|
|
199
|
+
replied: Boolean(row.reply_text),
|
|
200
|
+
publishedAt: row.published_at ?? void 0
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function toSummary(row) {
|
|
204
|
+
if (!row) return { average: 0, count: 0, distribution: [] };
|
|
205
|
+
return {
|
|
206
|
+
average: Number(row.average) || 0,
|
|
207
|
+
count: Number(row.count) || 0,
|
|
208
|
+
distribution: [5, 4, 3, 2, 1].map((stars) => ({
|
|
209
|
+
stars,
|
|
210
|
+
count: Number(row[`star${stars}`]) || 0
|
|
211
|
+
}))
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
var EMPTY_SUMMARY = { average: 0, count: 0, distribution: [] };
|
|
215
|
+
function toManagedReview(row) {
|
|
216
|
+
return {
|
|
217
|
+
...toReview(row),
|
|
218
|
+
date: humanDate(row.published_at ?? row.created_at),
|
|
219
|
+
publishedAt: row.published_at ?? row.created_at ?? void 0,
|
|
220
|
+
status: row.status ?? "pending"
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function toRow(input, tenantId) {
|
|
224
|
+
const row = {};
|
|
225
|
+
if (tenantId) row.tenant_id = tenantId;
|
|
226
|
+
if (input.productId !== void 0) row.product_id = input.productId;
|
|
227
|
+
if (input.productName !== void 0) row.product_name = input.productName;
|
|
228
|
+
if (input.productSlug !== void 0) row.product_slug = input.productSlug;
|
|
229
|
+
if (input.orderId !== void 0) row.order_id = input.orderId;
|
|
230
|
+
if (input.author !== void 0) row.author_name = input.author;
|
|
231
|
+
if (input.avatarUrl !== void 0) row.author_avatar_url = input.avatarUrl;
|
|
232
|
+
if (input.rating !== void 0) row.rating = input.rating;
|
|
233
|
+
if (input.title !== void 0) row.title = input.title;
|
|
234
|
+
if (input.text !== void 0) row.body = input.text;
|
|
235
|
+
if (input.source !== void 0) row.source = input.source;
|
|
236
|
+
if (input.status !== void 0) row.status = input.status;
|
|
237
|
+
if (input.verified !== void 0) row.verified = input.verified;
|
|
238
|
+
if (input.replyText !== void 0) {
|
|
239
|
+
row.reply_text = input.replyText;
|
|
240
|
+
row.replied_at = input.replyText ? (/* @__PURE__ */ new Date()).toISOString() : null;
|
|
241
|
+
}
|
|
242
|
+
return row;
|
|
243
|
+
}
|
|
244
|
+
function createSupabaseReputationProvider(options) {
|
|
245
|
+
const feed = (columns = "*") => getClient().from(REVIEWS_VIEW).select(columns).eq("tenant_id", resolveTenant(options));
|
|
246
|
+
return {
|
|
247
|
+
async listReviews(query) {
|
|
248
|
+
let q = feed().order("published_at", { ascending: false });
|
|
249
|
+
if (query?.minRating != null) q = q.gte("rating", query.minRating);
|
|
250
|
+
if (query?.limit != null) q = q.limit(query.limit);
|
|
251
|
+
const { data, error } = await q;
|
|
252
|
+
if (error) throw new Error(`[plugin-reputation] listReviews: ${error.message}`);
|
|
253
|
+
return (data ?? []).map(toReview);
|
|
254
|
+
},
|
|
255
|
+
async getSummary() {
|
|
256
|
+
const { data, error } = await feed("rating");
|
|
257
|
+
if (error) throw new Error(`[plugin-reputation] getSummary: ${error.message}`);
|
|
258
|
+
const ratings = (data ?? []).map((r) => Number(r.rating) || 0);
|
|
259
|
+
const count = ratings.length;
|
|
260
|
+
if (count === 0) return EMPTY_SUMMARY;
|
|
261
|
+
return {
|
|
262
|
+
average: Math.round(ratings.reduce((s, n) => s + n, 0) / count * 10) / 10,
|
|
263
|
+
count,
|
|
264
|
+
distribution: [5, 4, 3, 2, 1].map((stars) => ({
|
|
265
|
+
stars,
|
|
266
|
+
count: ratings.filter((n) => Math.round(n) === stars).length
|
|
267
|
+
}))
|
|
268
|
+
};
|
|
269
|
+
},
|
|
270
|
+
async getProductRating(productId) {
|
|
271
|
+
const { data, error } = await getClient().from(RATINGS_VIEW).select("*").eq("tenant_id", resolveTenant(options)).eq("product_id", productId).maybeSingle();
|
|
272
|
+
if (error) throw new Error(`[plugin-reputation] getProductRating: ${error.message}`);
|
|
273
|
+
return toSummary(data);
|
|
274
|
+
},
|
|
275
|
+
async getProductRatings(productIds) {
|
|
276
|
+
if (productIds.length === 0) return {};
|
|
277
|
+
const { data, error } = await getClient().from(RATINGS_VIEW).select("*").eq("tenant_id", resolveTenant(options)).in("product_id", [...productIds]);
|
|
278
|
+
if (error) throw new Error(`[plugin-reputation] getProductRatings: ${error.message}`);
|
|
279
|
+
const out = {};
|
|
280
|
+
for (const id of productIds) out[id] = EMPTY_SUMMARY;
|
|
281
|
+
for (const row of data ?? []) out[String(row.product_id)] = toSummary(row);
|
|
282
|
+
return out;
|
|
283
|
+
},
|
|
284
|
+
async listProductReviews(query) {
|
|
285
|
+
let q = feed().eq("product_id", query.productId);
|
|
286
|
+
q = query.sort === "rating" ? q.order("rating", { ascending: false }).order("published_at", { ascending: false }) : q.order("published_at", { ascending: false });
|
|
287
|
+
if (query.minRating != null) q = q.gte("rating", query.minRating);
|
|
288
|
+
if (query.limit != null) q = q.limit(query.limit);
|
|
289
|
+
const { data, error } = await q;
|
|
290
|
+
if (error) throw new Error(`[plugin-reputation] listProductReviews: ${error.message}`);
|
|
291
|
+
return (data ?? []).map(toReview);
|
|
292
|
+
},
|
|
293
|
+
async listTopReviews(query) {
|
|
294
|
+
let q = feed();
|
|
295
|
+
if (query?.productOnly ?? true) q = q.not("product_id", "is", null);
|
|
296
|
+
if (query?.productIds?.length) q = q.in("product_id", [...query.productIds]);
|
|
297
|
+
q = q.gte("rating", query?.minRating ?? 4).order("rating", { ascending: false }).order("published_at", { ascending: false }).limit(query?.limit ?? 6);
|
|
298
|
+
const { data, error } = await q;
|
|
299
|
+
if (error) throw new Error(`[plugin-reputation] listTopReviews: ${error.message}`);
|
|
300
|
+
return (data ?? []).map(toReview);
|
|
301
|
+
},
|
|
302
|
+
// --- backoffice ---------------------------------------------------------
|
|
303
|
+
// These hit the base table, where the only thing standing between a caller
|
|
304
|
+
// and another tenant's reviews is RLS. tenant_id is written explicitly so an
|
|
305
|
+
// INSERT is rejected by the WITH CHECK rather than silently landing nowhere.
|
|
306
|
+
async listManagedReviews(query) {
|
|
307
|
+
let q = getClient().from(TABLE).select("*").eq("tenant_id", resolveTenant(options)).order("created_at", { ascending: false });
|
|
308
|
+
if (query?.status) q = q.eq("status", query.status);
|
|
309
|
+
if (query?.productId) q = q.eq("product_id", query.productId);
|
|
310
|
+
if (query?.kind === "product") q = q.not("product_id", "is", null);
|
|
311
|
+
if (query?.kind === "business") q = q.is("product_id", null);
|
|
312
|
+
if (query?.minRating != null) q = q.gte("rating", query.minRating);
|
|
313
|
+
if (query?.limit != null) q = q.limit(query.limit);
|
|
314
|
+
const { data, error } = await q;
|
|
315
|
+
if (error) throw new Error(`[plugin-reputation] listManagedReviews: ${error.message}`);
|
|
316
|
+
return (data ?? []).map(toManagedReview);
|
|
317
|
+
},
|
|
318
|
+
async createReview(input) {
|
|
319
|
+
const { data, error } = await getClient().from(TABLE).insert(toRow(input, resolveTenant(options))).select("*").single();
|
|
320
|
+
if (error) throw new Error(`[plugin-reputation] createReview: ${error.message}`);
|
|
321
|
+
return toManagedReview(data);
|
|
322
|
+
},
|
|
323
|
+
async updateReview(id, patch) {
|
|
324
|
+
const { data, error } = await getClient().from(TABLE).update({ ...toRow(patch), updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id).select("*").single();
|
|
325
|
+
if (error) throw new Error(`[plugin-reputation] updateReview: ${error.message}`);
|
|
326
|
+
return toManagedReview(data);
|
|
327
|
+
},
|
|
328
|
+
async setReviewStatus(id, status) {
|
|
329
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
330
|
+
const { data, error } = await getClient().from(TABLE).update({
|
|
331
|
+
status,
|
|
332
|
+
updated_at: now,
|
|
333
|
+
// Approving is what publishes it; the timestamp is what the public
|
|
334
|
+
// view sorts by, so it is stamped here rather than at write time.
|
|
335
|
+
...status === "approved" ? { published_at: now } : {}
|
|
336
|
+
}).eq("id", id).select("*").single();
|
|
337
|
+
if (error) throw new Error(`[plugin-reputation] setReviewStatus: ${error.message}`);
|
|
338
|
+
return toManagedReview(data);
|
|
339
|
+
},
|
|
340
|
+
async deleteReview(id) {
|
|
341
|
+
const { error } = await getClient().from(TABLE).delete().eq("id", id);
|
|
342
|
+
if (error) throw new Error(`[plugin-reputation] deleteReview: ${error.message}`);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
function useManagedReviews(query) {
|
|
347
|
+
const { provider } = useReputationContext();
|
|
348
|
+
const [reviews, setReviews] = useState([]);
|
|
349
|
+
const [loading, setLoading] = useState(true);
|
|
350
|
+
const [error, setError] = useState(null);
|
|
351
|
+
const [nonce, setNonce] = useState(0);
|
|
352
|
+
const canModerate = typeof provider.listManagedReviews === "function";
|
|
353
|
+
const status = query?.status;
|
|
354
|
+
const kind = query?.kind;
|
|
355
|
+
const productId = query?.productId;
|
|
356
|
+
const limit = query?.limit;
|
|
357
|
+
useEffect(() => {
|
|
358
|
+
if (!provider.listManagedReviews) {
|
|
359
|
+
setReviews([]);
|
|
360
|
+
setLoading(false);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
let active = true;
|
|
364
|
+
setLoading(true);
|
|
365
|
+
provider.listManagedReviews({ status, kind, productId, limit }).then((result) => {
|
|
366
|
+
if (!active) return;
|
|
367
|
+
setReviews(result);
|
|
368
|
+
setError(null);
|
|
369
|
+
}).catch((err) => {
|
|
370
|
+
if (!active) return;
|
|
371
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
372
|
+
setReviews([]);
|
|
373
|
+
}).finally(() => {
|
|
374
|
+
if (active) setLoading(false);
|
|
375
|
+
});
|
|
376
|
+
return () => {
|
|
377
|
+
active = false;
|
|
378
|
+
};
|
|
379
|
+
}, [provider, status, kind, productId, limit, nonce]);
|
|
380
|
+
const refresh = useCallback(() => setNonce((n) => n + 1), []);
|
|
381
|
+
const setStatus = useCallback(
|
|
382
|
+
async (id, next) => {
|
|
383
|
+
if (!provider.setReviewStatus) return;
|
|
384
|
+
await provider.setReviewStatus(id, next);
|
|
385
|
+
refresh();
|
|
386
|
+
},
|
|
387
|
+
[provider, refresh]
|
|
388
|
+
);
|
|
389
|
+
const reply = useCallback(
|
|
390
|
+
async (id, text) => {
|
|
391
|
+
if (!provider.updateReview) return;
|
|
392
|
+
await provider.updateReview(id, { replyText: text });
|
|
393
|
+
refresh();
|
|
394
|
+
},
|
|
395
|
+
[provider, refresh]
|
|
396
|
+
);
|
|
397
|
+
const remove = useCallback(
|
|
398
|
+
async (id) => {
|
|
399
|
+
if (!provider.deleteReview) return;
|
|
400
|
+
await provider.deleteReview(id);
|
|
401
|
+
refresh();
|
|
402
|
+
},
|
|
403
|
+
[provider, refresh]
|
|
404
|
+
);
|
|
405
|
+
return { reviews, loading, error, canModerate, refresh, setStatus, reply, remove };
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
export { ReputationProvider, createMockReputationProvider, createSupabaseReputationProvider, useManagedReviews, useReputationContext, useReviewSummary };
|
|
409
|
+
//# sourceMappingURL=chunk-BGCEG4NA.js.map
|
|
410
|
+
//# sourceMappingURL=chunk-BGCEG4NA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.tsx","../src/hooks/useReviewSummary.ts","../src/data/mock.ts","../src/data/supabase.ts","../src/hooks/useManagedReviews.ts"],"names":["useState","useEffect"],"mappings":"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","file":"chunk-BGCEG4NA.js","sourcesContent":["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 { 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 type { ReputationDataProvider } from './types'\nimport type {\n Review,\n ReviewSummary,\n ReviewListQuery,\n ProductReviewQuery,\n TopReviewsQuery,\n ManagedReviewQuery,\n ReviewInput,\n ReviewStatus,\n} 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\nexport const SEED_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\nexport function computeSummary(reviews: readonly 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\n/** Newest first. Seeds without `publishedAt` keep their given order. */\nfunction byRecent(a: Review, b: Review): number {\n if (!a.publishedAt || !b.publishedAt) return 0\n return b.publishedAt.localeCompare(a.publishedAt)\n}\n\nfunction applyListQuery(reviews: readonly Review[], query?: ReviewListQuery): 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\nlet mockIdCounter = 0\n\nexport function createMockReputationProvider(options?: MockReputationProviderOptions): ReputationDataProvider {\n // A mutable copy: the backoffice writes land here so a seeded host behaves\n // like a real one for a whole session, instead of silently discarding edits.\n const reviews: Review[] = [...(options?.seed?.reviews ?? SEED_REVIEWS)]\n const summary: ReviewSummary = options?.seed?.summary ?? computeSummary(reviews)\n const forProduct = (productId: string): Review[] => reviews.filter((r) => r.productId === productId)\n\n return {\n async listReviews(query?: ReviewListQuery): Promise<Review[]> {\n return applyListQuery(reviews, query)\n },\n async getSummary(): Promise<ReviewSummary> {\n return summary\n },\n async getProductRating(productId: string): Promise<ReviewSummary> {\n return computeSummary(forProduct(productId))\n },\n async getProductRatings(productIds: readonly string[]): Promise<Record<string, ReviewSummary>> {\n const out: Record<string, ReviewSummary> = {}\n for (const id of productIds) out[id] = computeSummary(forProduct(id))\n return out\n },\n async listProductReviews(query: ProductReviewQuery): Promise<Review[]> {\n const sorted = forProduct(query.productId).sort(\n query.sort === 'rating' ? (a, b) => b.rating - a.rating : byRecent,\n )\n return applyListQuery(sorted, query)\n },\n async listTopReviews(query?: TopReviewsQuery): Promise<Review[]> {\n const productOnly = query?.productOnly ?? true\n let pool = reviews.filter((r) => (productOnly ? r.productId != null : true))\n if (query?.productIds?.length) {\n const wanted = new Set(query.productIds)\n pool = pool.filter((r) => r.productId != null && wanted.has(r.productId))\n }\n // Highest rated first, then newest — a social-proof strip leads with the best.\n pool = pool.sort((a, b) => b.rating - a.rating || byRecent(a, b))\n return applyListQuery(pool, { ...query, limit: query?.limit ?? 6 })\n },\n async listManagedReviews(query?: ManagedReviewQuery): Promise<Review[]> {\n let pool = reviews\n if (query?.status) pool = pool.filter((r) => (r.status ?? 'approved') === query.status)\n if (query?.productId) pool = pool.filter((r) => r.productId === query.productId)\n if (query?.kind === 'product') pool = pool.filter((r) => r.productId != null)\n if (query?.kind === 'business') pool = pool.filter((r) => r.productId == null)\n return applyListQuery([...pool].sort(byRecent), query)\n },\n\n async createReview(input: ReviewInput): Promise<Review> {\n const review: Review = {\n id: `mock-${++mockIdCounter}`,\n author: input.author,\n rating: input.rating,\n text: input.text ?? '',\n date: new Date().toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' }),\n publishedAt: new Date().toISOString(),\n source: input.source ?? 'Website',\n status: input.status ?? 'pending',\n productId: input.productId ?? undefined,\n productName: input.productName ?? undefined,\n productSlug: input.productSlug ?? undefined,\n title: input.title ?? undefined,\n verified: input.verified ?? false,\n avatarUrl: input.avatarUrl ?? undefined,\n replyText: input.replyText ?? undefined,\n replied: Boolean(input.replyText),\n }\n reviews.unshift(review)\n return review\n },\n\n async updateReview(id: string, patch: Partial<ReviewInput>): Promise<Review> {\n const i = reviews.findIndex((r) => r.id === id)\n if (i < 0) throw new Error(`[plugin-reputation] no review ${id}`)\n const next: Review = {\n ...reviews[i]!,\n ...(patch.author !== undefined ? { author: patch.author } : {}),\n ...(patch.rating !== undefined ? { rating: patch.rating } : {}),\n ...(patch.text !== undefined ? { text: patch.text } : {}),\n ...(patch.title !== undefined ? { title: patch.title ?? undefined } : {}),\n ...(patch.status !== undefined ? { status: patch.status } : {}),\n ...(patch.verified !== undefined ? { verified: patch.verified } : {}),\n ...(patch.replyText !== undefined\n ? { replyText: patch.replyText ?? undefined, replied: Boolean(patch.replyText) }\n : {}),\n }\n reviews[i] = next\n return next\n },\n\n async setReviewStatus(id: string, status: ReviewStatus): Promise<Review> {\n const i = reviews.findIndex((r) => r.id === id)\n if (i < 0) throw new Error(`[plugin-reputation] no review ${id}`)\n const next: Review = { ...reviews[i]!, status }\n reviews[i] = next\n return next\n },\n\n async deleteReview(id: string): Promise<void> {\n const i = reviews.findIndex((r) => r.id === id)\n if (i >= 0) reviews.splice(i, 1)\n },\n }\n}\n","import { getSupabaseClientOptional, getActiveTenantId } from '@fayz-ai/core'\nimport type { ReputationDataProvider } from './types'\nimport type {\n Review,\n ReviewSummary,\n ReviewListQuery,\n ProductReviewQuery,\n TopReviewsQuery,\n ManagedReviewQuery,\n ReviewInput,\n ReviewStatus,\n} from '../types'\n\n// ---------------------------------------------------------------------------\n// Supabase-backed reputation provider (public/storefront read side).\n//\n// Reads APPROVED reviews from the anon-safe views shipped in\n// migrations/001_product_reviews.sql, scoped to a tenant:\n//\n// v_public_product_reviews — the feed, column-whitelisted, approved only\n// v_public_product_ratings — the per-product aggregate, one row per product\n//\n// The backoffice writes plg_reputation_reviews directly under authenticated\n// tenant RLS; nothing here writes. The tenant comes from the configured\n// tenantId (anon storefront) or the active session tenant. Without a client or\n// a tenant we throw, and createSafeDataProvider falls back to mock/seed.\n// ---------------------------------------------------------------------------\n\nconst REVIEWS_VIEW = 'v_public_product_reviews'\nconst RATINGS_VIEW = 'v_public_product_ratings'\nconst TABLE = 'plg_reputation_reviews'\n\nexport interface SupabaseReputationProviderOptions {\n /** Tenant whose approved reviews to read. Defaults to the active session tenant. */\n tenantId?: string\n}\n\nfunction getClient() {\n const supabase = getSupabaseClientOptional() as { from: (t: string) => any } | null\n if (!supabase) throw new Error('[plugin-reputation] Supabase client not initialized')\n return supabase\n}\n\nfunction resolveTenant(options?: SupabaseReputationProviderOptions): string {\n const tenantId = options?.tenantId ?? getActiveTenantId()\n if (!tenantId) throw new Error('[plugin-reputation] No tenant to read reviews for')\n return tenantId\n}\n\n/** Human month-year label (pt-BR) derived from an ISO date, e.g. \"Janeiro 2025\". */\nfunction humanDate(iso?: string | null): string {\n if (!iso) return ''\n const d = new Date(iso)\n if (Number.isNaN(d.getTime())) return ''\n const label = d.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' })\n return label.charAt(0).toUpperCase() + label.slice(1)\n}\n\nfunction toReview(row: Record<string, any>): Review {\n return {\n id: String(row.id),\n author: row.author_name ?? 'Cliente',\n // PostgREST sends numeric as a JSON number, but a proxy that stringifies it\n // would otherwise turn every average into NaN downstream.\n rating: Number(row.rating) || 0,\n text: row.body ?? '',\n date: humanDate(row.published_at),\n source: row.source ?? undefined,\n productId: row.product_id ?? undefined,\n productName: row.product_name ?? undefined,\n productSlug: row.product_slug ?? undefined,\n title: row.title ?? undefined,\n verified: row.verified === true,\n avatarUrl: row.author_avatar_url ?? undefined,\n replyText: row.reply_text ?? undefined,\n replied: Boolean(row.reply_text),\n publishedAt: row.published_at ?? undefined,\n }\n}\n\nfunction toSummary(row: Record<string, any> | null | undefined): ReviewSummary {\n if (!row) return { average: 0, count: 0, distribution: [] }\n return {\n average: Number(row.average) || 0,\n count: Number(row.count) || 0,\n distribution: [5, 4, 3, 2, 1].map((stars) => ({\n stars,\n count: Number(row[`star${stars}`]) || 0,\n })),\n }\n}\n\nconst EMPTY_SUMMARY: ReviewSummary = { average: 0, count: 0, distribution: [] }\n\n/** Base-table row → Review. Same as the view mapper plus the moderation fields. */\nfunction toManagedReview(row: Record<string, any>): Review {\n return {\n ...toReview(row),\n date: humanDate(row.published_at ?? row.created_at),\n publishedAt: row.published_at ?? row.created_at ?? undefined,\n status: (row.status ?? 'pending') as ReviewStatus,\n }\n}\n\n/** Review input → base-table row. Undefined keys are left untouched by an update. */\nfunction toRow(input: Partial<ReviewInput>, tenantId?: string): Record<string, any> {\n const row: Record<string, any> = {}\n if (tenantId) row.tenant_id = tenantId\n if (input.productId !== undefined) row.product_id = input.productId\n if (input.productName !== undefined) row.product_name = input.productName\n if (input.productSlug !== undefined) row.product_slug = input.productSlug\n if (input.orderId !== undefined) row.order_id = input.orderId\n if (input.author !== undefined) row.author_name = input.author\n if (input.avatarUrl !== undefined) row.author_avatar_url = input.avatarUrl\n if (input.rating !== undefined) row.rating = input.rating\n if (input.title !== undefined) row.title = input.title\n if (input.text !== undefined) row.body = input.text\n if (input.source !== undefined) row.source = input.source\n if (input.status !== undefined) row.status = input.status\n if (input.verified !== undefined) row.verified = input.verified\n if (input.replyText !== undefined) {\n row.reply_text = input.replyText\n row.replied_at = input.replyText ? new Date().toISOString() : null\n }\n return row\n}\n\nexport function createSupabaseReputationProvider(\n options?: SupabaseReputationProviderOptions,\n): ReputationDataProvider {\n // Columns are a parameter because PostgREST's builder only offers `.select()`\n // once, off `from()` — chaining a second one throws at runtime, not compile time.\n const feed = (columns = '*') =>\n getClient().from(REVIEWS_VIEW).select(columns).eq('tenant_id', resolveTenant(options))\n\n return {\n async listReviews(query?: ReviewListQuery): Promise<Review[]> {\n let q = feed().order('published_at', { ascending: false })\n if (query?.minRating != null) q = q.gte('rating', query.minRating)\n if (query?.limit != null) q = q.limit(query.limit)\n const { data, error } = await q\n if (error) throw new Error(`[plugin-reputation] listReviews: ${error.message}`)\n return (data ?? []).map(toReview)\n },\n\n async getSummary(): Promise<ReviewSummary> {\n // The business headline counts every approved review, product ones\n // included — it is \"what people say about us\", not about one product.\n const { data, error } = await feed('rating')\n if (error) throw new Error(`[plugin-reputation] getSummary: ${error.message}`)\n const ratings = (data ?? []).map((r: Record<string, any>) => Number(r.rating) || 0)\n const count = ratings.length\n if (count === 0) return EMPTY_SUMMARY\n return {\n average: Math.round((ratings.reduce((s: number, n: number) => s + n, 0) / count) * 10) / 10,\n count,\n distribution: [5, 4, 3, 2, 1].map((stars) => ({\n stars,\n count: ratings.filter((n: number) => Math.round(n) === stars).length,\n })),\n }\n },\n\n async getProductRating(productId: string): Promise<ReviewSummary> {\n const { data, error } = await getClient()\n .from(RATINGS_VIEW)\n .select('*')\n .eq('tenant_id', resolveTenant(options))\n .eq('product_id', productId)\n .maybeSingle()\n if (error) throw new Error(`[plugin-reputation] getProductRating: ${error.message}`)\n return toSummary(data)\n },\n\n async getProductRatings(productIds: readonly string[]): Promise<Record<string, ReviewSummary>> {\n if (productIds.length === 0) return {}\n const { data, error } = await getClient()\n .from(RATINGS_VIEW)\n .select('*')\n .eq('tenant_id', resolveTenant(options))\n .in('product_id', [...productIds])\n if (error) throw new Error(`[plugin-reputation] getProductRatings: ${error.message}`)\n const out: Record<string, ReviewSummary> = {}\n // A product with no approved reviews has no row in the view; the caller\n // still gets an entry, so a card can render \"no reviews yet\" rather than\n // treating the gap as a loading state that never resolves.\n for (const id of productIds) out[id] = EMPTY_SUMMARY\n for (const row of data ?? []) out[String(row.product_id)] = toSummary(row)\n return out\n },\n\n async listProductReviews(query: ProductReviewQuery): Promise<Review[]> {\n let q = feed().eq('product_id', query.productId)\n q = query.sort === 'rating'\n ? q.order('rating', { ascending: false }).order('published_at', { ascending: false })\n : q.order('published_at', { ascending: false })\n if (query.minRating != null) q = q.gte('rating', query.minRating)\n if (query.limit != null) q = q.limit(query.limit)\n const { data, error } = await q\n if (error) throw new Error(`[plugin-reputation] listProductReviews: ${error.message}`)\n return (data ?? []).map(toReview)\n },\n\n async listTopReviews(query?: TopReviewsQuery): Promise<Review[]> {\n let q = feed()\n if (query?.productOnly ?? true) q = q.not('product_id', 'is', null)\n if (query?.productIds?.length) q = q.in('product_id', [...query.productIds])\n q = q\n .gte('rating', query?.minRating ?? 4)\n .order('rating', { ascending: false })\n .order('published_at', { ascending: false })\n .limit(query?.limit ?? 6)\n const { data, error } = await q\n if (error) throw new Error(`[plugin-reputation] listTopReviews: ${error.message}`)\n return (data ?? []).map(toReview)\n },\n // --- backoffice ---------------------------------------------------------\n // These hit the base table, where the only thing standing between a caller\n // and another tenant's reviews is RLS. tenant_id is written explicitly so an\n // INSERT is rejected by the WITH CHECK rather than silently landing nowhere.\n\n async listManagedReviews(query?: ManagedReviewQuery): Promise<Review[]> {\n let q = getClient()\n .from(TABLE)\n .select('*')\n .eq('tenant_id', resolveTenant(options))\n .order('created_at', { ascending: false })\n if (query?.status) q = q.eq('status', query.status)\n if (query?.productId) q = q.eq('product_id', query.productId)\n if (query?.kind === 'product') q = q.not('product_id', 'is', null)\n if (query?.kind === 'business') q = q.is('product_id', null)\n if (query?.minRating != null) q = q.gte('rating', query.minRating)\n if (query?.limit != null) q = q.limit(query.limit)\n const { data, error } = await q\n if (error) throw new Error(`[plugin-reputation] listManagedReviews: ${error.message}`)\n return (data ?? []).map(toManagedReview)\n },\n\n async createReview(input: ReviewInput): Promise<Review> {\n const { data, error } = await getClient()\n .from(TABLE)\n .insert(toRow(input, resolveTenant(options)))\n .select('*')\n .single()\n if (error) throw new Error(`[plugin-reputation] createReview: ${error.message}`)\n return toManagedReview(data)\n },\n\n async updateReview(id: string, patch: Partial<ReviewInput>): Promise<Review> {\n const { data, error } = await getClient()\n .from(TABLE)\n .update({ ...toRow(patch), updated_at: new Date().toISOString() })\n .eq('id', id)\n .select('*')\n .single()\n if (error) throw new Error(`[plugin-reputation] updateReview: ${error.message}`)\n return toManagedReview(data)\n },\n\n async setReviewStatus(id: string, status: ReviewStatus): Promise<Review> {\n const now = new Date().toISOString()\n const { data, error } = await getClient()\n .from(TABLE)\n .update({\n status,\n updated_at: now,\n // Approving is what publishes it; the timestamp is what the public\n // view sorts by, so it is stamped here rather than at write time.\n ...(status === 'approved' ? { published_at: now } : {}),\n })\n .eq('id', id)\n .select('*')\n .single()\n if (error) throw new Error(`[plugin-reputation] setReviewStatus: ${error.message}`)\n return toManagedReview(data)\n },\n\n async deleteReview(id: string): Promise<void> {\n const { error } = await getClient().from(TABLE).delete().eq('id', id)\n if (error) throw new Error(`[plugin-reputation] deleteReview: ${error.message}`)\n },\n }\n}\n","import { useCallback, useEffect, useState } from 'react'\nimport { useReputationContext } from '../context'\nimport type { ManagedReviewQuery, Review, ReviewStatus } from '../types'\n\nexport interface UseManagedReviewsResult {\n reviews: Review[]\n loading: boolean\n error: Error | null\n /** False when the active provider is read-only (the anon storefront one). */\n canModerate: boolean\n refresh: () => void\n setStatus: (id: string, status: ReviewStatus) => Promise<void>\n reply: (id: string, text: string) => Promise<void>\n remove: (id: string) => Promise<void>\n}\n\n/**\n * The backoffice list: every review, whatever its status.\n *\n * Separate from `useReviews` because that one reads the public, approved-only\n * view — a moderation queue built on it could never show the thing it exists to\n * moderate. Mutations refetch rather than patch local state: status changes also\n * move `published_at`, and guessing that client-side is how a list drifts from\n * the table it claims to show.\n */\nexport function useManagedReviews(query?: ManagedReviewQuery): UseManagedReviewsResult {\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 const [nonce, setNonce] = useState(0)\n\n const canModerate = typeof provider.listManagedReviews === 'function'\n const status = query?.status\n const kind = query?.kind\n const productId = query?.productId\n const limit = query?.limit\n\n useEffect(() => {\n if (!provider.listManagedReviews) {\n setReviews([])\n setLoading(false)\n return\n }\n let active = true\n setLoading(true)\n provider\n .listManagedReviews({ status, kind, productId, limit })\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 setReviews([])\n })\n .finally(() => {\n if (active) setLoading(false)\n })\n return () => {\n active = false\n }\n }, [provider, status, kind, productId, limit, nonce])\n\n const refresh = useCallback(() => setNonce((n) => n + 1), [])\n\n const setStatus = useCallback(\n async (id: string, next: ReviewStatus) => {\n if (!provider.setReviewStatus) return\n await provider.setReviewStatus(id, next)\n refresh()\n },\n [provider, refresh],\n )\n\n const reply = useCallback(\n async (id: string, text: string) => {\n if (!provider.updateReview) return\n await provider.updateReview(id, { replyText: text })\n refresh()\n },\n [provider, refresh],\n )\n\n const remove = useCallback(\n async (id: string) => {\n if (!provider.deleteReview) return\n await provider.deleteReview(id)\n refresh()\n },\n [provider, refresh],\n )\n\n return { reviews, loading, error, canModerate, refresh, setStatus, reply, remove }\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ProductRatingProps {
|
|
2
|
+
productId?: string;
|
|
3
|
+
/** Pre-fetched aggregate. Skips the per-card query. */
|
|
4
|
+
average?: number;
|
|
5
|
+
count?: number;
|
|
6
|
+
size?: number;
|
|
7
|
+
/** Hide the "(12)" tail. */
|
|
8
|
+
hideCount?: boolean;
|
|
9
|
+
/** What to render when nothing has been reviewed yet. Default: nothing. */
|
|
10
|
+
emptyLabel?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
onClick?: () => void;
|
|
13
|
+
}
|
|
14
|
+
export declare function ProductRating({ productId, average, count, size, hideCount, emptyLabel, className, onClick, }: ProductRatingProps): import("react").JSX.Element | null;
|
|
15
|
+
//# sourceMappingURL=ProductRating.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductRating.d.ts","sourceRoot":"","sources":["../../src/components/ProductRating.tsx"],"names":[],"mappings":"AAWA,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,4BAA4B;IAC5B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;CACrB;AAID,wBAAgB,aAAa,CAAC,EAC5B,SAAS,EACT,OAAO,EACP,KAAK,EACL,IAAS,EACT,SAAiB,EACjB,UAAU,EACV,SAAS,EACT,OAAO,GACR,EAAE,kBAAkB,sCAoCpB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ProductReviewsProps {
|
|
2
|
+
productId: string;
|
|
3
|
+
heading?: string;
|
|
4
|
+
/** How many to show. Default 6. */
|
|
5
|
+
limit?: number;
|
|
6
|
+
sort?: 'recent' | 'rating';
|
|
7
|
+
/** Rendered when the product has no approved reviews. Default: a quiet line. */
|
|
8
|
+
emptyState?: React.ReactNode;
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function ProductReviews({ productId, heading, limit, sort, emptyState, className, }: ProductReviewsProps): import("react").JSX.Element | null;
|
|
12
|
+
//# sourceMappingURL=ProductReviews.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProductReviews.d.ts","sourceRoot":"","sources":["../../src/components/ProductReviews.tsx"],"names":[],"mappings":"AAYA,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC1B,gFAAgF;IAChF,UAAU,CAAC,EAAE,KAAK,CAAC,SAAS,CAAA;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAiDD,wBAAgB,cAAc,CAAC,EAC7B,SAAS,EACT,OAAsB,EACtB,KAAS,EACT,IAAe,EACf,UAAU,EACV,SAAS,GACV,EAAE,mBAAmB,sCA4CrB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface StarsProps {
|
|
2
|
+
/** 0–5. Rendered to the nearest half. */
|
|
3
|
+
value: number;
|
|
4
|
+
/** Pixel size of one star. Default 16. */
|
|
5
|
+
size?: number;
|
|
6
|
+
/** Colour of a filled star. Default a warm amber. */
|
|
7
|
+
color?: string;
|
|
8
|
+
/** Colour of the empty remainder. Default a neutral grey. */
|
|
9
|
+
emptyColor?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
/** Screen-reader label. Pass null to hide it when an adjacent number says it. */
|
|
12
|
+
label?: string | null;
|
|
13
|
+
}
|
|
14
|
+
export declare function Stars({ value, size, color, emptyColor, className, label }: StarsProps): import("react").JSX.Element;
|
|
15
|
+
//# sourceMappingURL=Stars.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Stars.d.ts","sourceRoot":"","sources":["../../src/components/Stars.tsx"],"names":[],"mappings":"AAWA,MAAM,WAAW,UAAU;IACzB,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAA;IACb,0CAA0C;IAC1C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,qDAAqD;IACrD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,iFAAiF;IACjF,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB;AAcD,wBAAgB,KAAK,CAAC,EAAE,KAAK,EAAE,IAAS,EAAE,KAAiB,EAAE,UAAsB,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,UAAU,+BA0BlH"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { Review, TopReviewsQuery } from '../types';
|
|
3
|
+
export interface TopReviewsProps extends TopReviewsQuery {
|
|
4
|
+
heading?: string;
|
|
5
|
+
subheading?: string;
|
|
6
|
+
/** Build the PDP link for a review's product. Omit to render unlinked cards. */
|
|
7
|
+
productHref?: (review: Review) => string | undefined;
|
|
8
|
+
/** Rendered instead of the section when there is nothing to show. Default: nothing. */
|
|
9
|
+
emptyState?: ReactNode;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function TopReviews({ heading, subheading, productHref, emptyState, className, ...query }: TopReviewsProps): import("react").JSX.Element | null;
|
|
13
|
+
//# sourceMappingURL=TopReviews.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TopReviews.d.ts","sourceRoot":"","sources":["../../src/components/TopReviews.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAGrD,OAAO,KAAK,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAYvD,MAAM,WAAW,eAAgB,SAAQ,eAAe;IACtD,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,gFAAgF;IAChF,WAAW,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;IACpD,uFAAuF;IACvF,UAAU,CAAC,EAAE,SAAS,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAgDD,wBAAgB,UAAU,CAAC,EACzB,OAAuC,EACvC,UAAU,EACV,WAAW,EACX,UAAU,EACV,SAAS,EACT,GAAG,KAAK,EACT,EAAE,eAAe,sCAiBjB"}
|