@mohasinac/appkit 4.41.0 → 4.41.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/features/shell/SectionForm.js +44 -5
- package/package.json +2 -2
- package/dist/_internal/server/features/classified/actions.d.ts +0 -12
- package/dist/_internal/server/features/classified/actions.js +0 -33
- package/dist/_internal/server/features/digital-code/actions.d.ts +0 -12
- package/dist/_internal/server/features/digital-code/actions.js +0 -33
- package/dist/_internal/server/features/homepage/data.d.ts +0 -18
- package/dist/_internal/server/features/homepage/data.js +0 -70
- package/dist/_internal/server/features/live/actions.d.ts +0 -6
- package/dist/_internal/server/features/live/actions.js +0 -27
- package/dist/_internal/server/features/messages/actions.d.ts +0 -6
- package/dist/_internal/server/features/messages/actions.js +0 -6
- package/dist/_internal/server/features/messages/data.d.ts +0 -10
- package/dist/_internal/server/features/messages/data.js +0 -24
- package/dist/_internal/server/features/messages/index.d.ts +0 -2
- package/dist/_internal/server/features/messages/index.js +0 -2
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.d.ts +0 -12
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +0 -112
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.js +0 -2
- package/dist/_internal/shared/features/homepage/section-gate.d.ts +0 -14
- package/dist/_internal/shared/features/homepage/section-gate.js +0 -51
- package/dist/_internal/shared/features/lottery/config-form.d.ts +0 -58
- package/dist/_internal/shared/features/lottery/config-form.js +0 -71
- package/dist/client/api/useFeatureFlags.d.ts +0 -22
- package/dist/client/api/useFeatureFlags.js +0 -27
- package/dist/features/account/address-validation.d.ts +0 -20
- package/dist/features/account/address-validation.js +0 -35
- package/dist/features/account/components/ChatList.d.ts +0 -14
- package/dist/features/account/components/ChatList.js +0 -9
- package/dist/features/account/components/ChatWindow.d.ts +0 -17
- package/dist/features/account/components/ChatWindow.js +0 -6
- package/dist/features/account/components/MessagesView.d.ts +0 -16
- package/dist/features/account/components/MessagesView.js +0 -5
- package/dist/features/account/components/UserOffersView.d.ts +0 -10
- package/dist/features/account/components/UserOffersView.js +0 -5
- package/dist/features/account/constants/addresses.d.ts +0 -13
- package/dist/features/account/constants/addresses.js +0 -43
- package/dist/features/account/hooks/useAddressForm.d.ts +0 -24
- package/dist/features/account/hooks/useAddressForm.js +0 -58
- package/dist/features/admin/actions/chat-actions.d.ts +0 -29
- package/dist/features/admin/actions/chat-actions.js +0 -101
- package/dist/features/admin/components/AdminFeatureFlagsView.d.ts +0 -9
- package/dist/features/admin/components/AdminFeatureFlagsView.js +0 -109
- package/dist/features/admin/components/AdminSiteView.d.ts +0 -10
- package/dist/features/admin/components/AdminSiteView.js +0 -52
- package/dist/features/admin/hooks/useChat.d.ts +0 -24
- package/dist/features/admin/hooks/useChat.js +0 -100
- package/dist/features/admin/permission-map.d.ts +0 -3
- package/dist/features/admin/permission-map.js +0 -26
- package/dist/features/admin/repository/chat.repository.d.ts +0 -50
- package/dist/features/admin/repository/chat.repository.js +0 -223
- package/dist/features/auth/actions/password-change-otp-actions.d.ts +0 -18
- package/dist/features/auth/actions/password-change-otp-actions.js +0 -88
- package/dist/features/auth/actions/realtime-token-actions.d.ts +0 -11
- package/dist/features/auth/actions/realtime-token-actions.js +0 -38
- package/dist/features/auth/password-change-otp.d.ts +0 -34
- package/dist/features/auth/password-change-otp.js +0 -56
- package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +0 -12
- package/dist/features/classified/components/ClassifiedContactSellerPanel.js +0 -49
- package/dist/features/events/api/route.d.ts +0 -12
- package/dist/features/events/api/route.js +0 -90
- package/dist/features/faq/api/route.d.ts +0 -11
- package/dist/features/faq/api/route.js +0 -52
- package/dist/features/faq/hooks/useFAQs.d.ts +0 -22
- package/dist/features/faq/hooks/useFAQs.js +0 -41
- package/dist/features/media/upload/video-poster.test.d.ts +0 -1
- package/dist/features/media/upload/video-poster.test.js +0 -42
- package/dist/features/messages/actions/index.d.ts +0 -2
- package/dist/features/messages/actions/index.js +0 -2
- package/dist/features/messages/actions/messages-actions.d.ts +0 -15
- package/dist/features/messages/actions/messages-actions.js +0 -33
- package/dist/features/messages/actions/ping-rtdb.d.ts +0 -2
- package/dist/features/messages/actions/ping-rtdb.js +0 -26
- package/dist/features/messages/hooks/useConversation.d.ts +0 -22
- package/dist/features/messages/hooks/useConversation.js +0 -155
- package/dist/features/messages/hooks/useConversations.d.ts +0 -15
- package/dist/features/messages/hooks/useConversations.js +0 -104
- package/dist/features/messages/index.d.ts +0 -6
- package/dist/features/messages/index.js +0 -4
- package/dist/features/messages/realtime.d.ts +0 -32
- package/dist/features/messages/realtime.js +0 -39
- package/dist/features/messages/repository/conversations.repository.d.ts +0 -71
- package/dist/features/messages/repository/conversations.repository.js +0 -284
- package/dist/features/messages/schemas/firestore.d.ts +0 -34
- package/dist/features/messages/schemas/firestore.js +0 -16
- package/dist/features/messages/schemas/index.d.ts +0 -177
- package/dist/features/messages/schemas/index.js +0 -35
- package/dist/features/messages/server.d.ts +0 -6
- package/dist/features/messages/server.js +0 -6
- package/dist/features/products/components/MakeOfferForm.d.ts +0 -20
- package/dist/features/products/components/MakeOfferForm.js +0 -9
- package/dist/features/reviews/api/route.d.ts +0 -21
- package/dist/features/reviews/api/route.js +0 -174
- package/dist/features/seller/api/offers/route.d.ts +0 -17
- package/dist/features/seller/api/offers/route.js +0 -63
- package/dist/features/seller/components/SellerAuctionsView.d.ts +0 -9
- package/dist/features/seller/components/SellerAuctionsView.js +0 -113
- package/dist/features/seller/components/SellerOffersPanel.d.ts +0 -21
- package/dist/features/seller/components/SellerOffersPanel.js +0 -160
- package/dist/features/seller/components/SellerPreOrdersView.d.ts +0 -6
- package/dist/features/seller/components/SellerPreOrdersView.js +0 -148
- package/dist/features/seller/components/SellerPrizeDrawsView.d.ts +0 -6
- package/dist/features/seller/components/SellerPrizeDrawsView.js +0 -152
- package/dist/features/seller/permission-map.d.ts +0 -5
- package/dist/features/seller/permission-map.js +0 -17
- package/dist/features/shell/StepForm.d.ts +0 -80
- package/dist/features/shell/StepForm.js +0 -136
- package/dist/features/stores/api/route.d.ts +0 -12
- package/dist/features/stores/api/route.js +0 -110
- package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.js +0 -33
- package/dist/features/tester/seed-data/authored/account-auth__testing-program.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/account-auth__testing-program.js +0 -138
- package/dist/features/tester/seed-data/bids-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/bids-tester-seed-data.js +0 -37
- package/dist/features/tester/seed-data/blog-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/blog-tester-seed-data.js +0 -40
- package/dist/features/tester/seed-data/carts-tester-seed-data.d.ts +0 -3
- package/dist/features/tester/seed-data/carts-tester-seed-data.js +0 -70
- package/dist/features/tester/seed-data/categories-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +0 -218
- package/dist/features/tester/seed-data/coupons-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/coupons-tester-seed-data.js +0 -76
- package/dist/features/tester/seed-data/events-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/events-tester-seed-data.js +0 -193
- package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +0 -191
- package/dist/features/tester/seed-data/orders-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +0 -329
- package/dist/features/tester/seed-data/products-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/products-tester-seed-data.js +0 -815
- package/dist/features/tester/seed-data/stores-tester-seed-data.d.ts +0 -2
- package/dist/features/tester/seed-data/stores-tester-seed-data.js +0 -79
- package/dist/features/wishlist/components/WishlistToggleButton.d.ts +0 -12
- package/dist/features/wishlist/components/WishlistToggleButton.js +0 -36
- package/dist/monitoring/analytics.d.ts +0 -80
- package/dist/monitoring/analytics.js +0 -176
- package/dist/next/middleware/pii-redactor.d.ts +0 -32
- package/dist/next/middleware/pii-redactor.js +0 -84
- package/dist/providers/db-firebase/realtime.d.ts +0 -42
- package/dist/providers/db-firebase/realtime.js +0 -138
- package/dist/security/rbac/Can.d.ts +0 -31
- package/dist/security/rbac/Can.js +0 -22
- package/dist/security/rbac/default-roles.d.ts +0 -2
- package/dist/security/rbac/default-roles.js +0 -83
- package/dist/security/rbac/hook.d.ts +0 -20
- package/dist/security/rbac/hook.js +0 -30
- package/dist/security/rbac/index.d.ts +0 -8
- package/dist/security/rbac/index.js +0 -6
- package/dist/security/rbac/middleware.d.ts +0 -51
- package/dist/security/rbac/middleware.js +0 -51
- package/dist/security/rbac/resolver.d.ts +0 -5
- package/dist/security/rbac/resolver.js +0 -38
- package/dist/security/rbac/server.d.ts +0 -24
- package/dist/security/rbac/server.js +0 -42
- package/dist/security/rbac/types.d.ts +0 -22
- package/dist/security/rbac/types.js +0 -2
- package/dist/seed/conversations-seed-data.d.ts +0 -2
- package/dist/seed/conversations-seed-data.js +0 -292
- package/dist/utils/filter.helper.d.ts +0 -18
- package/dist/utils/filter.helper.js +0 -21
- package/dist/utils/pagination.helper.d.ts +0 -23
- package/dist/utils/pagination.helper.js +0 -24
- package/dist/utils/search-tokens.d.ts +0 -2
- package/dist/utils/search-tokens.js +0 -21
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ConversationsRepository — buyer↔seller messaging.
|
|
3
|
-
*
|
|
4
|
-
* One document per buyer×storeId×productId tuple. Messages are embedded in
|
|
5
|
-
* the document as an array (good up to ~200 entries; migrate to subcollection
|
|
6
|
-
* later if threads explode).
|
|
7
|
-
*
|
|
8
|
-
* All mutations are transactional so concurrent writes don't drop messages
|
|
9
|
-
* or unread counters.
|
|
10
|
-
*/
|
|
11
|
-
import { getAdminDb } from "../../../providers/db-firebase";
|
|
12
|
-
import { encryptPiiFields, decryptPiiFields } from "../../../security/pii-encrypt";
|
|
13
|
-
import { CONVERSATION_PII_FIELDS } from "../../../security/pii-schemas";
|
|
14
|
-
import { serverLogger } from "../../../monitoring";
|
|
15
|
-
import { CONVERSATION_FIELDS } from "../../../constants/field-names";
|
|
16
|
-
import { CONVERSATIONS_COLLECTION, } from "../schemas/firestore";
|
|
17
|
-
function toDate(raw) {
|
|
18
|
-
if (raw instanceof Date)
|
|
19
|
-
return raw;
|
|
20
|
-
if (raw && typeof raw.toDate === "function") {
|
|
21
|
-
return raw.toDate();
|
|
22
|
-
}
|
|
23
|
-
if (typeof raw === "string" || typeof raw === "number")
|
|
24
|
-
return new Date(raw);
|
|
25
|
-
return new Date();
|
|
26
|
-
}
|
|
27
|
-
function normaliseMessage(raw) {
|
|
28
|
-
const r = (raw ?? {});
|
|
29
|
-
return {
|
|
30
|
-
id: String(r.id ?? ""),
|
|
31
|
-
senderId: String(r.senderId ?? ""),
|
|
32
|
-
senderRole: r.senderRole ?? "buyer",
|
|
33
|
-
body: String(r.body ?? ""),
|
|
34
|
-
isRead: r.isRead === true,
|
|
35
|
-
sentAt: toDate(r.sentAt),
|
|
36
|
-
attachments: Array.isArray(r.attachments)
|
|
37
|
-
? r.attachments
|
|
38
|
-
: undefined,
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function normaliseDoc(id, raw) {
|
|
42
|
-
// Decrypt here rather than at the six call sites: this is the one function
|
|
43
|
-
// every read path already goes through, so a new reader cannot forget it.
|
|
44
|
-
// `markRead` deliberately does NOT decrypt — it reads raw messages, flips
|
|
45
|
-
// isRead and writes them straight back, so ciphertext round-trips untouched.
|
|
46
|
-
const r = decryptPiiFields(raw, [...CONVERSATION_PII_FIELDS]);
|
|
47
|
-
return {
|
|
48
|
-
id,
|
|
49
|
-
buyerId: String(r.buyerId ?? ""),
|
|
50
|
-
buyerDisplayName: String(r.buyerDisplayName ?? ""),
|
|
51
|
-
storeId: String(r.storeId ?? ""),
|
|
52
|
-
storeName: String(r.storeName ?? ""),
|
|
53
|
-
sellerDisplayName: String(r.sellerDisplayName ?? ""),
|
|
54
|
-
productId: typeof r.productId === "string" ? r.productId : undefined,
|
|
55
|
-
productTitle: typeof r.productTitle === "string" ? r.productTitle : undefined,
|
|
56
|
-
messages: Array.isArray(r.messages) ? r.messages.map(normaliseMessage) : [],
|
|
57
|
-
lastMessage: String(r.lastMessage ?? ""),
|
|
58
|
-
lastMessageAt: toDate(r.lastMessageAt),
|
|
59
|
-
unreadBuyer: typeof r.unreadBuyer === "number" ? r.unreadBuyer : 0,
|
|
60
|
-
unreadSeller: typeof r.unreadSeller === "number" ? r.unreadSeller : 0,
|
|
61
|
-
status: r.status ?? "active",
|
|
62
|
-
createdAt: toDate(r.createdAt),
|
|
63
|
-
updatedAt: toDate(r.updatedAt),
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
export class ConversationFullError extends Error {
|
|
67
|
-
constructor() {
|
|
68
|
-
super(...arguments);
|
|
69
|
-
this.code = "CONVERSATION_FULL";
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
export class ConversationsRepository {
|
|
73
|
-
collection() {
|
|
74
|
-
return getAdminDb().collection(CONVERSATIONS_COLLECTION);
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Find an existing buyer×store×product conversation or create a new one.
|
|
78
|
-
* Idempotent — safe to call on every "Contact Seller" click.
|
|
79
|
-
*/
|
|
80
|
-
async findOrCreateByContext(params) {
|
|
81
|
-
const db = getAdminDb();
|
|
82
|
-
const coll = this.collection();
|
|
83
|
-
const { buyerId, storeId, productId } = params;
|
|
84
|
-
// Stable composite key: buyerId + storeId + (productId | "general")
|
|
85
|
-
const productKey = productId ?? "general";
|
|
86
|
-
const stableId = `conv-${buyerId}-${storeId}-${productKey}`;
|
|
87
|
-
const ref = coll.doc(stableId);
|
|
88
|
-
return db.runTransaction(async (tx) => {
|
|
89
|
-
const snap = await tx.get(ref);
|
|
90
|
-
if (snap.exists) {
|
|
91
|
-
return normaliseDoc(snap.id, snap.data() ?? {});
|
|
92
|
-
}
|
|
93
|
-
const now = new Date();
|
|
94
|
-
const newDoc = {
|
|
95
|
-
buyerId: params.buyerId,
|
|
96
|
-
buyerDisplayName: params.buyerDisplayName,
|
|
97
|
-
storeId: params.storeId,
|
|
98
|
-
storeName: params.storeName,
|
|
99
|
-
sellerDisplayName: params.sellerDisplayName,
|
|
100
|
-
productId: params.productId,
|
|
101
|
-
productTitle: params.productTitle,
|
|
102
|
-
messages: [],
|
|
103
|
-
lastMessage: "",
|
|
104
|
-
lastMessageAt: now,
|
|
105
|
-
unreadBuyer: 0,
|
|
106
|
-
unreadSeller: 0,
|
|
107
|
-
status: "active",
|
|
108
|
-
createdAt: now,
|
|
109
|
-
updatedAt: now,
|
|
110
|
-
};
|
|
111
|
-
const encryptedDoc = encryptPiiFields(newDoc, [...CONVERSATION_PII_FIELDS]);
|
|
112
|
-
tx.set(ref, encryptedDoc);
|
|
113
|
-
return normaliseDoc(stableId, encryptedDoc);
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
async findById(id) {
|
|
117
|
-
const snap = await this.collection().doc(id).get();
|
|
118
|
-
if (!snap.exists)
|
|
119
|
-
return null;
|
|
120
|
-
return normaliseDoc(snap.id, snap.data() ?? {});
|
|
121
|
-
}
|
|
122
|
-
async listByBuyer(buyerId) {
|
|
123
|
-
const snap = await this.collection()
|
|
124
|
-
.where(CONVERSATION_FIELDS.BUYER_ID, "==", buyerId)
|
|
125
|
-
.orderBy(CONVERSATION_FIELDS.LAST_MESSAGE_AT, "desc")
|
|
126
|
-
.get();
|
|
127
|
-
return snap.docs.map((d) => normaliseDoc(d.id, d.data()));
|
|
128
|
-
}
|
|
129
|
-
async listByStore(storeId) {
|
|
130
|
-
const snap = await this.collection()
|
|
131
|
-
.where(CONVERSATION_FIELDS.STORE_ID, "==", storeId)
|
|
132
|
-
.orderBy(CONVERSATION_FIELDS.LAST_MESSAGE_AT, "desc")
|
|
133
|
-
.get();
|
|
134
|
-
return snap.docs.map((d) => normaliseDoc(d.id, d.data()));
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Conversation ids this user may subscribe to, for the RTDB `conversationIds`
|
|
138
|
-
* custom-token claim.
|
|
139
|
-
*
|
|
140
|
-
* 🛑 This claim previously had NO ISSUER ANYWHERE. The `chats/$conversationId`
|
|
141
|
-
* rule requires `auth.token.conversationIds[$conversationId] == true`, but
|
|
142
|
-
* `/api/realtime/token` only ever issued `chatIds`, built by
|
|
143
|
-
* `chatRepository.getChatIdsForUser()` over the **`chatRooms`** collection —
|
|
144
|
-
* a different collection with different document ids. So the buyer↔seller
|
|
145
|
-
* live-message channel was denied for every user, and both subscribing hooks
|
|
146
|
-
* swallowed the permission error (one into `setIsConnected(false)`, one into
|
|
147
|
-
* a literal `// ignore`). Messages only ever appeared for the sender, via an
|
|
148
|
-
* unrelated `refetch()` after send.
|
|
149
|
-
*
|
|
150
|
-
* `.select()` with no fields fetches ids only — the document bodies are not
|
|
151
|
-
* needed and this runs on every token issue.
|
|
152
|
-
*
|
|
153
|
-
* @param limit hard cap. Firebase custom claims are limited to 1000 BYTES
|
|
154
|
-
* total; each entry costs roughly `"<20-char id>":true,` ≈ 28 bytes, so a
|
|
155
|
-
* user in hundreds of conversations would make `createCustomToken` throw
|
|
156
|
-
* and take the whole route down (see the route's own comment).
|
|
157
|
-
*/
|
|
158
|
-
async getConversationIdsForUser(userId, storeIds = [], limit = 25) {
|
|
159
|
-
const queries = [
|
|
160
|
-
this.collection()
|
|
161
|
-
.where(CONVERSATION_FIELDS.BUYER_ID, "==", userId)
|
|
162
|
-
.orderBy(CONVERSATION_FIELDS.LAST_MESSAGE_AT, "desc")
|
|
163
|
-
.limit(limit)
|
|
164
|
-
.select()
|
|
165
|
-
.get(),
|
|
166
|
-
// Firestore `in` caps at 30 values; a seller with more stores than that
|
|
167
|
-
// is not a shape this app produces.
|
|
168
|
-
...(storeIds.length > 0
|
|
169
|
-
? [
|
|
170
|
-
this.collection()
|
|
171
|
-
.where(CONVERSATION_FIELDS.STORE_ID, "in", storeIds.slice(0, 30))
|
|
172
|
-
.orderBy(CONVERSATION_FIELDS.LAST_MESSAGE_AT, "desc")
|
|
173
|
-
.limit(limit)
|
|
174
|
-
.select()
|
|
175
|
-
.get(),
|
|
176
|
-
]
|
|
177
|
-
: []),
|
|
178
|
-
];
|
|
179
|
-
const snaps = await Promise.all(queries);
|
|
180
|
-
const ids = new Set();
|
|
181
|
-
for (const snap of snaps) {
|
|
182
|
-
for (const doc of snap.docs)
|
|
183
|
-
ids.add(doc.id);
|
|
184
|
-
}
|
|
185
|
-
// Most-recent-first is already the sort order; truncating here keeps the
|
|
186
|
-
// claim inside the byte budget when a user is both buyer and seller.
|
|
187
|
-
return [...ids].slice(0, limit);
|
|
188
|
-
}
|
|
189
|
-
/**
|
|
190
|
-
* Append a message to a conversation in a transaction. Bumps the counterparty
|
|
191
|
-
* unread counter; updates lastMessage / lastMessageAt / updatedAt.
|
|
192
|
-
*/
|
|
193
|
-
async appendMessage(conversationId, msg) {
|
|
194
|
-
const db = getAdminDb();
|
|
195
|
-
const ref = this.collection().doc(conversationId);
|
|
196
|
-
try {
|
|
197
|
-
return await db.runTransaction(async (tx) => {
|
|
198
|
-
const snap = await tx.get(ref);
|
|
199
|
-
if (!snap.exists) {
|
|
200
|
-
throw new Error("Conversation not found");
|
|
201
|
-
}
|
|
202
|
-
const data = snap.data() ?? {};
|
|
203
|
-
const existing = Array.isArray(data.messages)
|
|
204
|
-
? data.messages.map(normaliseMessage)
|
|
205
|
-
: [];
|
|
206
|
-
const now = msg.sentAt ?? new Date();
|
|
207
|
-
const newMessage = {
|
|
208
|
-
id: msg.id ?? `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
209
|
-
senderId: msg.senderId,
|
|
210
|
-
senderRole: msg.senderRole,
|
|
211
|
-
body: msg.body,
|
|
212
|
-
isRead: false,
|
|
213
|
-
sentAt: now,
|
|
214
|
-
attachments: msg.attachments,
|
|
215
|
-
};
|
|
216
|
-
const nextMessages = [...existing, newMessage];
|
|
217
|
-
const unreadBuyer = msg.senderRole === "seller"
|
|
218
|
-
? (typeof data.unreadBuyer === "number" ? data.unreadBuyer : 0) + 1
|
|
219
|
-
: (typeof data.unreadBuyer === "number" ? data.unreadBuyer : 0);
|
|
220
|
-
const unreadSeller = msg.senderRole === "buyer"
|
|
221
|
-
? (typeof data.unreadSeller === "number" ? data.unreadSeller : 0) + 1
|
|
222
|
-
: (typeof data.unreadSeller === "number" ? data.unreadSeller : 0);
|
|
223
|
-
const patch = {
|
|
224
|
-
messages: nextMessages,
|
|
225
|
-
lastMessage: msg.body,
|
|
226
|
-
lastMessageAt: now,
|
|
227
|
-
unreadBuyer,
|
|
228
|
-
unreadSeller,
|
|
229
|
-
updatedAt: now,
|
|
230
|
-
};
|
|
231
|
-
// Encrypt before the write; normaliseDoc decrypts on the way back out,
|
|
232
|
-
// so the caller still receives plaintext.
|
|
233
|
-
const encryptedPatch = encryptPiiFields(patch, [...CONVERSATION_PII_FIELDS]);
|
|
234
|
-
tx.update(ref, encryptedPatch);
|
|
235
|
-
return normaliseDoc(conversationId, { ...data, ...encryptedPatch });
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
catch (error) {
|
|
239
|
-
serverLogger.error("ConversationsRepository.appendMessage error", {
|
|
240
|
-
conversationId,
|
|
241
|
-
error,
|
|
242
|
-
});
|
|
243
|
-
throw error;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
/**
|
|
247
|
-
* Zero out the unread counter for one party. Also flips `isRead` on the
|
|
248
|
-
* embedded messages sent by the other party so the seller side can see what
|
|
249
|
-
* the buyer has already read.
|
|
250
|
-
*/
|
|
251
|
-
async markRead(conversationId, role) {
|
|
252
|
-
const db = getAdminDb();
|
|
253
|
-
const ref = this.collection().doc(conversationId);
|
|
254
|
-
try {
|
|
255
|
-
await db.runTransaction(async (tx) => {
|
|
256
|
-
const snap = await tx.get(ref);
|
|
257
|
-
if (!snap.exists)
|
|
258
|
-
return;
|
|
259
|
-
const data = snap.data() ?? {};
|
|
260
|
-
const messages = Array.isArray(data.messages)
|
|
261
|
-
? data.messages.map(normaliseMessage)
|
|
262
|
-
: [];
|
|
263
|
-
// role="buyer" means the buyer just opened the chat → mark all
|
|
264
|
-
// seller-sent messages read.
|
|
265
|
-
const otherRole = role === "buyer" ? "seller" : "buyer";
|
|
266
|
-
const nextMessages = messages.map((m) => m.senderRole === otherRole && !m.isRead ? { ...m, isRead: true } : m);
|
|
267
|
-
tx.update(ref, {
|
|
268
|
-
messages: nextMessages,
|
|
269
|
-
[role === "buyer" ? "unreadBuyer" : "unreadSeller"]: 0,
|
|
270
|
-
updatedAt: new Date(),
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
catch (error) {
|
|
275
|
-
serverLogger.error("ConversationsRepository.markRead error", {
|
|
276
|
-
conversationId,
|
|
277
|
-
role,
|
|
278
|
-
error,
|
|
279
|
-
});
|
|
280
|
-
throw error;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
export const conversationsRepository = new ConversationsRepository();
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Messages / Conversations Firestore Document Types & Constants
|
|
3
|
-
*
|
|
4
|
-
* Architecture: one `conversations` document per buyer↔seller pair on a product.
|
|
5
|
-
* Messages are embedded as an array for seed purposes; production can migrate to
|
|
6
|
-
* a subcollection when the thread exceeds ~200 messages.
|
|
7
|
-
*/
|
|
8
|
-
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
9
|
-
export type ConversationMessage = {
|
|
10
|
-
id: string;
|
|
11
|
-
senderId: string;
|
|
12
|
-
senderRole: "buyer" | "seller";
|
|
13
|
-
body: string;
|
|
14
|
-
isRead: boolean;
|
|
15
|
-
sentAt: Date;
|
|
16
|
-
attachments?: string[];
|
|
17
|
-
};
|
|
18
|
-
export interface ConversationDocument extends BaseDocument {
|
|
19
|
-
buyerId: string;
|
|
20
|
-
buyerDisplayName: string;
|
|
21
|
-
storeId: string;
|
|
22
|
-
storeName: string;
|
|
23
|
-
sellerDisplayName: string;
|
|
24
|
-
productId?: string;
|
|
25
|
-
productTitle?: string;
|
|
26
|
-
messages: ConversationMessage[];
|
|
27
|
-
lastMessage: string;
|
|
28
|
-
lastMessageAt: Date;
|
|
29
|
-
unreadBuyer: number;
|
|
30
|
-
unreadSeller: number;
|
|
31
|
-
status: "active" | "archived" | "blocked";
|
|
32
|
-
}
|
|
33
|
-
export declare const CONVERSATIONS_COLLECTION: "conversations";
|
|
34
|
-
export declare const CONVERSATIONS_INDEXED_FIELDS: readonly ["buyerId", "storeId", "productId", "status", "lastMessageAt", "createdAt"];
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Messages / Conversations Firestore Document Types & Constants
|
|
3
|
-
*
|
|
4
|
-
* Architecture: one `conversations` document per buyer↔seller pair on a product.
|
|
5
|
-
* Messages are embedded as an array for seed purposes; production can migrate to
|
|
6
|
-
* a subcollection when the thread exceeds ~200 messages.
|
|
7
|
-
*/
|
|
8
|
-
export const CONVERSATIONS_COLLECTION = "conversations";
|
|
9
|
-
export const CONVERSATIONS_INDEXED_FIELDS = [
|
|
10
|
-
"buyerId",
|
|
11
|
-
"storeId",
|
|
12
|
-
"productId",
|
|
13
|
-
"status",
|
|
14
|
-
"lastMessageAt",
|
|
15
|
-
"createdAt",
|
|
16
|
-
];
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
export * from "./firestore";
|
|
3
|
-
export declare const conversationMessageSenderRoleSchema: z.ZodEnum<["buyer", "seller"]>;
|
|
4
|
-
export declare const conversationStatusSchema: z.ZodEnum<["active", "archived", "blocked"]>;
|
|
5
|
-
export declare const conversationMessageSchema: z.ZodObject<{
|
|
6
|
-
id: z.ZodString;
|
|
7
|
-
senderId: z.ZodString;
|
|
8
|
-
senderRole: z.ZodEnum<["buyer", "seller"]>;
|
|
9
|
-
body: z.ZodString;
|
|
10
|
-
isRead: z.ZodBoolean;
|
|
11
|
-
sentAt: z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
|
|
12
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
13
|
-
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
14
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
15
|
-
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
16
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
17
|
-
}, z.ZodTypeAny, "passthrough">>]>;
|
|
18
|
-
attachments: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
19
|
-
}, "strip", z.ZodTypeAny, {
|
|
20
|
-
id: string;
|
|
21
|
-
body: string;
|
|
22
|
-
isRead: boolean;
|
|
23
|
-
senderId: string;
|
|
24
|
-
senderRole: "buyer" | "seller";
|
|
25
|
-
sentAt: string | Date | z.objectOutputType<{
|
|
26
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
27
|
-
}, z.ZodTypeAny, "passthrough">;
|
|
28
|
-
attachments?: string[] | undefined;
|
|
29
|
-
}, {
|
|
30
|
-
id: string;
|
|
31
|
-
body: string;
|
|
32
|
-
isRead: boolean;
|
|
33
|
-
senderId: string;
|
|
34
|
-
senderRole: "buyer" | "seller";
|
|
35
|
-
sentAt: string | Date | z.objectInputType<{
|
|
36
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
37
|
-
}, z.ZodTypeAny, "passthrough">;
|
|
38
|
-
attachments?: string[] | undefined;
|
|
39
|
-
}>;
|
|
40
|
-
export declare const conversationFirestoreSchema: z.ZodObject<{
|
|
41
|
-
createdAt: z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
|
|
42
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
43
|
-
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
44
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
45
|
-
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
46
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
47
|
-
}, z.ZodTypeAny, "passthrough">>]>;
|
|
48
|
-
updatedAt: z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
|
|
49
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
50
|
-
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
51
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
52
|
-
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
53
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
54
|
-
}, z.ZodTypeAny, "passthrough">>]>;
|
|
55
|
-
id: z.ZodString;
|
|
56
|
-
buyerId: z.ZodString;
|
|
57
|
-
buyerDisplayName: z.ZodString;
|
|
58
|
-
storeId: z.ZodString;
|
|
59
|
-
storeName: z.ZodString;
|
|
60
|
-
sellerDisplayName: z.ZodString;
|
|
61
|
-
productId: z.ZodOptional<z.ZodString>;
|
|
62
|
-
productTitle: z.ZodOptional<z.ZodString>;
|
|
63
|
-
messages: z.ZodArray<z.ZodObject<{
|
|
64
|
-
id: z.ZodString;
|
|
65
|
-
senderId: z.ZodString;
|
|
66
|
-
senderRole: z.ZodEnum<["buyer", "seller"]>;
|
|
67
|
-
body: z.ZodString;
|
|
68
|
-
isRead: z.ZodBoolean;
|
|
69
|
-
sentAt: z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
|
|
70
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
71
|
-
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
72
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
73
|
-
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
74
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
75
|
-
}, z.ZodTypeAny, "passthrough">>]>;
|
|
76
|
-
attachments: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
77
|
-
}, "strip", z.ZodTypeAny, {
|
|
78
|
-
id: string;
|
|
79
|
-
body: string;
|
|
80
|
-
isRead: boolean;
|
|
81
|
-
senderId: string;
|
|
82
|
-
senderRole: "buyer" | "seller";
|
|
83
|
-
sentAt: string | Date | z.objectOutputType<{
|
|
84
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
85
|
-
}, z.ZodTypeAny, "passthrough">;
|
|
86
|
-
attachments?: string[] | undefined;
|
|
87
|
-
}, {
|
|
88
|
-
id: string;
|
|
89
|
-
body: string;
|
|
90
|
-
isRead: boolean;
|
|
91
|
-
senderId: string;
|
|
92
|
-
senderRole: "buyer" | "seller";
|
|
93
|
-
sentAt: string | Date | z.objectInputType<{
|
|
94
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
95
|
-
}, z.ZodTypeAny, "passthrough">;
|
|
96
|
-
attachments?: string[] | undefined;
|
|
97
|
-
}>, "many">;
|
|
98
|
-
lastMessage: z.ZodString;
|
|
99
|
-
lastMessageAt: z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
|
|
100
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
101
|
-
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
102
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
103
|
-
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
104
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
105
|
-
}, z.ZodTypeAny, "passthrough">>]>;
|
|
106
|
-
unreadBuyer: z.ZodNumber;
|
|
107
|
-
unreadSeller: z.ZodNumber;
|
|
108
|
-
status: z.ZodEnum<["active", "archived", "blocked"]>;
|
|
109
|
-
}, "strip", z.ZodTypeAny, {
|
|
110
|
-
status: "archived" | "active" | "blocked";
|
|
111
|
-
id: string;
|
|
112
|
-
createdAt: string | Date | z.objectOutputType<{
|
|
113
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
114
|
-
}, z.ZodTypeAny, "passthrough">;
|
|
115
|
-
updatedAt: string | Date | z.objectOutputType<{
|
|
116
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
117
|
-
}, z.ZodTypeAny, "passthrough">;
|
|
118
|
-
storeId: string;
|
|
119
|
-
storeName: string;
|
|
120
|
-
buyerId: string;
|
|
121
|
-
lastMessageAt: string | Date | z.objectOutputType<{
|
|
122
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
123
|
-
}, z.ZodTypeAny, "passthrough">;
|
|
124
|
-
lastMessage: string;
|
|
125
|
-
messages: {
|
|
126
|
-
id: string;
|
|
127
|
-
body: string;
|
|
128
|
-
isRead: boolean;
|
|
129
|
-
senderId: string;
|
|
130
|
-
senderRole: "buyer" | "seller";
|
|
131
|
-
sentAt: string | Date | z.objectOutputType<{
|
|
132
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
133
|
-
}, z.ZodTypeAny, "passthrough">;
|
|
134
|
-
attachments?: string[] | undefined;
|
|
135
|
-
}[];
|
|
136
|
-
buyerDisplayName: string;
|
|
137
|
-
sellerDisplayName: string;
|
|
138
|
-
unreadBuyer: number;
|
|
139
|
-
unreadSeller: number;
|
|
140
|
-
productId?: string | undefined;
|
|
141
|
-
productTitle?: string | undefined;
|
|
142
|
-
}, {
|
|
143
|
-
status: "archived" | "active" | "blocked";
|
|
144
|
-
id: string;
|
|
145
|
-
createdAt: string | Date | z.objectInputType<{
|
|
146
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
147
|
-
}, z.ZodTypeAny, "passthrough">;
|
|
148
|
-
updatedAt: string | Date | z.objectInputType<{
|
|
149
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
150
|
-
}, z.ZodTypeAny, "passthrough">;
|
|
151
|
-
storeId: string;
|
|
152
|
-
storeName: string;
|
|
153
|
-
buyerId: string;
|
|
154
|
-
lastMessageAt: string | Date | z.objectInputType<{
|
|
155
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
156
|
-
}, z.ZodTypeAny, "passthrough">;
|
|
157
|
-
lastMessage: string;
|
|
158
|
-
messages: {
|
|
159
|
-
id: string;
|
|
160
|
-
body: string;
|
|
161
|
-
isRead: boolean;
|
|
162
|
-
senderId: string;
|
|
163
|
-
senderRole: "buyer" | "seller";
|
|
164
|
-
sentAt: string | Date | z.objectInputType<{
|
|
165
|
-
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
166
|
-
}, z.ZodTypeAny, "passthrough">;
|
|
167
|
-
attachments?: string[] | undefined;
|
|
168
|
-
}[];
|
|
169
|
-
buyerDisplayName: string;
|
|
170
|
-
sellerDisplayName: string;
|
|
171
|
-
unreadBuyer: number;
|
|
172
|
-
unreadSeller: number;
|
|
173
|
-
productId?: string | undefined;
|
|
174
|
-
productTitle?: string | undefined;
|
|
175
|
-
}>;
|
|
176
|
-
export type ConversationFromSchema = z.infer<typeof conversationFirestoreSchema>;
|
|
177
|
-
export type ConversationMessageFromSchema = z.infer<typeof conversationMessageSchema>;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
// [SCHEMA] messages / conversations feature — Firestore document schema.
|
|
2
|
-
//
|
|
3
|
-
// Mirrors ConversationDocument + ConversationMessage in ./firestore.ts.
|
|
4
|
-
// Registered into SCHEMAS.firestore.conversations.
|
|
5
|
-
import { z } from "zod";
|
|
6
|
-
import { auditTimestampsShape, firestoreDateSchema } from "../../../schemas/firestore-helpers";
|
|
7
|
-
export * from "./firestore";
|
|
8
|
-
export const conversationMessageSenderRoleSchema = z.enum(["buyer", "seller"]);
|
|
9
|
-
export const conversationStatusSchema = z.enum(["active", "archived", "blocked"]);
|
|
10
|
-
export const conversationMessageSchema = z.object({
|
|
11
|
-
id: z.string(),
|
|
12
|
-
senderId: z.string(),
|
|
13
|
-
senderRole: conversationMessageSenderRoleSchema,
|
|
14
|
-
body: z.string(),
|
|
15
|
-
isRead: z.boolean(),
|
|
16
|
-
sentAt: firestoreDateSchema,
|
|
17
|
-
attachments: z.array(z.string()).optional(),
|
|
18
|
-
});
|
|
19
|
-
export const conversationFirestoreSchema = z.object({
|
|
20
|
-
id: z.string(),
|
|
21
|
-
buyerId: z.string(),
|
|
22
|
-
buyerDisplayName: z.string(),
|
|
23
|
-
storeId: z.string(),
|
|
24
|
-
storeName: z.string(),
|
|
25
|
-
sellerDisplayName: z.string(),
|
|
26
|
-
productId: z.string().optional(),
|
|
27
|
-
productTitle: z.string().optional(),
|
|
28
|
-
messages: z.array(conversationMessageSchema),
|
|
29
|
-
lastMessage: z.string(),
|
|
30
|
-
lastMessageAt: firestoreDateSchema,
|
|
31
|
-
unreadBuyer: z.number().int().nonnegative(),
|
|
32
|
-
unreadSeller: z.number().int().nonnegative(),
|
|
33
|
-
status: conversationStatusSchema,
|
|
34
|
-
...auditTimestampsShape,
|
|
35
|
-
});
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import React from "react";
|
|
2
|
-
export interface MakeOfferFormProps {
|
|
3
|
-
listedPrice?: number;
|
|
4
|
-
isLoading?: boolean;
|
|
5
|
-
isSubmitted?: boolean;
|
|
6
|
-
/** Render the offer amount input */
|
|
7
|
-
renderInput?: () => React.ReactNode;
|
|
8
|
-
/** Render the create / submit action button */
|
|
9
|
-
renderAction?: () => React.ReactNode;
|
|
10
|
-
/** Render terms acceptance checkbox or note */
|
|
11
|
-
renderTerms?: () => React.ReactNode;
|
|
12
|
-
/** Render success confirmation after offer is placed */
|
|
13
|
-
renderSuccess?: () => React.ReactNode;
|
|
14
|
-
/** Render auth gate when user is not authenticated */
|
|
15
|
-
renderAuthGate?: () => React.ReactNode;
|
|
16
|
-
/** Render validation error message */
|
|
17
|
-
renderError?: () => React.ReactNode;
|
|
18
|
-
className?: string;
|
|
19
|
-
}
|
|
20
|
-
export declare function MakeOfferForm({ isSubmitted, renderInput, renderAction, renderTerms, renderSuccess, renderAuthGate, renderError, className, }: MakeOfferFormProps): React.JSX.Element;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Stack } from "@mohasinac/appkit/client";
|
|
3
|
-
export function MakeOfferForm({ isSubmitted = false, renderInput, renderAction, renderTerms, renderSuccess, renderAuthGate, renderError, className = "", }) {
|
|
4
|
-
if (renderAuthGate)
|
|
5
|
-
return _jsx(_Fragment, { children: renderAuthGate() });
|
|
6
|
-
if (isSubmitted && renderSuccess)
|
|
7
|
-
return _jsx(_Fragment, { children: renderSuccess() });
|
|
8
|
-
return (_jsxs(Stack, { className: `${className}`, gap: "md", children: [renderError?.(), renderInput?.(), renderTerms?.(), renderAction?.()] }));
|
|
9
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* feat-reviews — Next.js App Router API handler (GET/POST /api/reviews)
|
|
3
|
-
*
|
|
4
|
-
* 2-line stub for GET:
|
|
5
|
-
* ```ts
|
|
6
|
-
* // app/api/reviews/route.ts
|
|
7
|
-
* export { GET, POST } from "@mohasinac/feat-reviews";
|
|
8
|
-
* ```
|
|
9
|
-
*
|
|
10
|
-
* Query modes for GET:
|
|
11
|
-
* ?featured=true → returns flat Review[] (for testimonial sections)
|
|
12
|
-
* ?latest=true → paginated approved reviews, no productId required
|
|
13
|
-
* ?productId=<id> → paginated + aggregate stats (averageRating, ratingDistribution)
|
|
14
|
-
*
|
|
15
|
-
* POST creates a new review; requires authentication.
|
|
16
|
-
*/
|
|
17
|
-
import { NextResponse } from "next/server.js";
|
|
18
|
-
export declare function GET(request: Request): Promise<NextResponse>;
|
|
19
|
-
export declare const POST: (request: Request, context?: {
|
|
20
|
-
params: Promise<Record<string, string>>;
|
|
21
|
-
} | undefined) => Promise<Response>;
|