@mohasinac/appkit 2.7.35 → 2.7.36
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/_internal/server/features/auctions/og.d.ts +1 -0
- package/dist/_internal/server/features/auctions/og.js +2 -1
- package/dist/_internal/server/features/blog/og.d.ts +1 -0
- package/dist/_internal/server/features/blog/og.js +3 -2
- package/dist/_internal/server/features/brands/og.d.ts +1 -0
- package/dist/_internal/server/features/brands/og.js +2 -1
- package/dist/_internal/server/features/bundles/og.d.ts +1 -0
- package/dist/_internal/server/features/bundles/og.js +2 -1
- package/dist/_internal/server/features/categories/og.d.ts +1 -0
- package/dist/_internal/server/features/categories/og.js +2 -1
- package/dist/_internal/server/features/checkout/actions.js +13 -5
- package/dist/_internal/server/features/classified/og.d.ts +2 -0
- package/dist/_internal/server/features/classified/og.js +2 -1
- package/dist/_internal/server/features/digital-code/og.d.ts +2 -0
- package/dist/_internal/server/features/digital-code/og.js +2 -1
- package/dist/_internal/server/features/events/og.d.ts +1 -0
- package/dist/_internal/server/features/events/og.js +3 -1
- package/dist/_internal/server/features/live/og.d.ts +2 -0
- package/dist/_internal/server/features/live/og.js +2 -1
- package/dist/_internal/server/features/pre-orders/og.d.ts +1 -0
- package/dist/_internal/server/features/pre-orders/og.js +2 -1
- package/dist/_internal/server/features/products/og.d.ts +3 -1
- package/dist/_internal/server/features/products/og.js +2 -1
- package/dist/_internal/server/features/seo/index.d.ts +1 -1
- package/dist/_internal/server/features/seo/index.js +1 -1
- package/dist/_internal/server/features/seo/og.d.ts +8 -1
- package/dist/_internal/server/features/seo/og.js +28 -8
- package/dist/_internal/server/features/stores/og.d.ts +1 -0
- package/dist/_internal/server/features/stores/og.js +3 -2
- package/dist/_internal/server/features/sublisting-categories/og.d.ts +1 -0
- package/dist/_internal/server/features/sublisting-categories/og.js +2 -1
- package/dist/_internal/server/jobs/core/onScamReportCreate.js +18 -14
- package/dist/_internal/server/jobs/core/onScamReportRejected.js +10 -7
- package/dist/_internal/server/jobs/core/onScamReportVerified.js +10 -7
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +9 -0
- package/dist/constants/api-endpoints.js +3 -0
- package/dist/features/admin/components/AdminBundlesView.js +20 -2
- package/dist/features/admin/components/AdminPayoutsView.js +2 -1
- package/dist/features/admin/components/AdminProductsView.js +13 -0
- package/dist/features/admin/components/AdminStoresView.js +43 -5
- package/dist/features/admin/components/AdminUsersView.js +53 -6
- package/dist/features/admin/schemas/firestore.d.ts +1 -1
- package/dist/features/blog/components/BlogPostForm.js +1 -0
- package/dist/features/categories/components/CategoryForm.js +1 -0
- package/dist/features/homepage/components/SectionCarousel.js +1 -8
- package/dist/seed/homepage-sections-seed-data.js +10 -10
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/ui/components/FormField.js +1 -0
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ interface AuctionDocLike {
|
|
|
14
14
|
export declare function renderAuctionOg(doc: AuctionDocLike | null | undefined, opts: {
|
|
15
15
|
siteName: string;
|
|
16
16
|
locale?: string;
|
|
17
|
+
baseUrl?: string;
|
|
17
18
|
}): ReactElement;
|
|
18
19
|
export declare function renderAuctionOgImage(data: AuctionOgData, siteName: string): ReactElement;
|
|
19
20
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
2
3
|
/** High-level OG renderer — accepts the raw auction document from `getAuctionForDetail`. */
|
|
3
4
|
export function renderAuctionOg(doc, opts) {
|
|
4
5
|
const locale = opts.locale ?? "en-IN";
|
|
@@ -13,7 +14,7 @@ export function renderAuctionOg(doc, opts) {
|
|
|
13
14
|
return renderAuctionOgImage({
|
|
14
15
|
title: doc?.title ?? "Live Auction",
|
|
15
16
|
endsLabel,
|
|
16
|
-
imageUrl: doc?.mainImage || doc?.images?.[0] || null,
|
|
17
|
+
imageUrl: resolveOgImageUrl(doc?.mainImage || doc?.images?.[0] || null, opts.baseUrl),
|
|
17
18
|
}, opts.siteName);
|
|
18
19
|
}
|
|
19
20
|
export function renderAuctionOgImage(data, siteName) {
|
|
@@ -18,6 +18,7 @@ interface BlogDocLike {
|
|
|
18
18
|
/** High-level OG renderer — accepts the raw blog post document from `getBlogPostForDetail`. */
|
|
19
19
|
export declare function renderBlogOg(doc: BlogDocLike | null | undefined, opts: {
|
|
20
20
|
siteName: string;
|
|
21
|
+
baseUrl?: string;
|
|
21
22
|
}): ReactElement;
|
|
22
23
|
export declare function renderBlogOgImage(data: BlogOgData, siteName: string): ReactElement;
|
|
23
24
|
export {};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
2
3
|
/** High-level OG renderer — accepts the raw blog post document from `getBlogPostForDetail`. */
|
|
3
4
|
export function renderBlogOg(doc, opts) {
|
|
4
|
-
const
|
|
5
|
+
const rawCoverImage = typeof doc?.coverImage === "string"
|
|
5
6
|
? doc.coverImage
|
|
6
7
|
: doc?.coverImage?.url ?? null;
|
|
7
8
|
return renderBlogOgImage({
|
|
@@ -9,7 +10,7 @@ export function renderBlogOg(doc, opts) {
|
|
|
9
10
|
excerpt: doc?.excerpt?.slice(0, 120) ?? null,
|
|
10
11
|
authorName: doc?.authorName ?? null,
|
|
11
12
|
category: doc?.category ?? null,
|
|
12
|
-
coverImage,
|
|
13
|
+
coverImage: resolveOgImageUrl(rawCoverImage, opts.baseUrl),
|
|
13
14
|
}, opts.siteName);
|
|
14
15
|
}
|
|
15
16
|
export function renderBlogOgImage(data, siteName) {
|
|
@@ -12,6 +12,7 @@ interface BrandDocLike {
|
|
|
12
12
|
/** High-level OG renderer — accepts the raw brand document from `getBrandForDetail`. */
|
|
13
13
|
export declare function renderBrandOg(doc: BrandDocLike | null | undefined, opts: {
|
|
14
14
|
siteName: string;
|
|
15
|
+
baseUrl?: string;
|
|
15
16
|
}): ReactElement;
|
|
16
17
|
export declare function renderBrandOgImage(data: BrandOgData, siteName: string): ReactElement;
|
|
17
18
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
2
3
|
/** High-level OG renderer — accepts the raw brand document from `getBrandForDetail`. */
|
|
3
4
|
export function renderBrandOg(doc, opts) {
|
|
4
5
|
const name = doc?.name ?? "Brand";
|
|
@@ -6,7 +7,7 @@ export function renderBrandOg(doc, opts) {
|
|
|
6
7
|
name,
|
|
7
8
|
description: doc?.description?.slice(0, 120) ??
|
|
8
9
|
`Shop authentic ${name} collectibles on ${opts.siteName}.`,
|
|
9
|
-
logoUrl: doc?.logoURL ?? null,
|
|
10
|
+
logoUrl: resolveOgImageUrl(doc?.logoURL ?? null, opts.baseUrl),
|
|
10
11
|
}, opts.siteName);
|
|
11
12
|
}
|
|
12
13
|
export function renderBrandOgImage(data, siteName) {
|
|
@@ -33,6 +33,7 @@ interface BundleDocLike {
|
|
|
33
33
|
}
|
|
34
34
|
export declare function renderBundleOg(doc: BundleDocLike | null | undefined, opts: {
|
|
35
35
|
siteName: string;
|
|
36
|
+
baseUrl?: string;
|
|
36
37
|
}): ReactElement;
|
|
37
38
|
export declare function renderBundleOgImage(data: BundleOgData, siteName: string): ReactElement;
|
|
38
39
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
2
3
|
function formatPriceInr(paise) {
|
|
3
4
|
const rupees = Math.round(paise / 100);
|
|
4
5
|
return `₹${rupees.toLocaleString("en-IN")}`;
|
|
@@ -12,7 +13,7 @@ export function renderBundleOg(doc, opts) {
|
|
|
12
13
|
name,
|
|
13
14
|
description: doc?.description?.slice(0, 140) ??
|
|
14
15
|
`Curated multi-product bundle on ${opts.siteName}.`,
|
|
15
|
-
coverImageUrl: doc?.display?.coverImage ?? null,
|
|
16
|
+
coverImageUrl: resolveOgImageUrl(doc?.display?.coverImage ?? null, opts.baseUrl),
|
|
16
17
|
priceLabel,
|
|
17
18
|
itemCount: doc?.bundleProductIds?.length ?? null,
|
|
18
19
|
stockStatus: doc?.bundleStockStatus ?? null,
|
|
@@ -28,6 +28,7 @@ interface CategoryDocLike {
|
|
|
28
28
|
}
|
|
29
29
|
export declare function renderCategoryOg(doc: CategoryDocLike | null | undefined, opts: {
|
|
30
30
|
siteName: string;
|
|
31
|
+
baseUrl?: string;
|
|
31
32
|
}): ReactElement;
|
|
32
33
|
export declare function renderCategoryOgImage(data: CategoryOgData, siteName: string): ReactElement;
|
|
33
34
|
export {};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
2
3
|
export function renderCategoryOg(doc, opts) {
|
|
3
4
|
const name = doc?.name ?? "Category";
|
|
4
5
|
return renderCategoryOgImage({
|
|
5
6
|
name,
|
|
6
7
|
description: doc?.description?.slice(0, 140) ??
|
|
7
8
|
`Browse ${name} on ${opts.siteName}.`,
|
|
8
|
-
coverImageUrl: doc?.display?.coverImage ?? null,
|
|
9
|
+
coverImageUrl: resolveOgImageUrl(doc?.display?.coverImage ?? null, opts.baseUrl),
|
|
9
10
|
productCount: doc?.metrics?.totalItemCount ?? null,
|
|
10
11
|
}, opts.siteName);
|
|
11
12
|
}
|
|
@@ -334,11 +334,18 @@ export async function createCheckoutOrderAction(input) {
|
|
|
334
334
|
const orderIds = [];
|
|
335
335
|
let total = 0;
|
|
336
336
|
const emailsToSend = [];
|
|
337
|
-
|
|
337
|
+
// SB-UNI-5 2026-05-13 — bundle cart-lines use item.price (locked bundle
|
|
338
|
+
// price at add-time); regular lines use product.price (current Firestore).
|
|
339
|
+
// This prevents stale cart-cached prices from being charged on COD/UPI orders.
|
|
340
|
+
const unitPriceFor = (item, product) => item.bundleCategorySlug && item.bundleProductIds?.length
|
|
341
|
+
? item.price
|
|
342
|
+
: product.price;
|
|
343
|
+
const cartSubtotal = orderGroups.reduce((s, { items: g }) => s + g.reduce((gs, { item, product }) => gs + unitPriceFor(item, product) * item.quantity, 0), 0);
|
|
338
344
|
const couponUsageAccumulator = new Map();
|
|
339
345
|
for (const { items: group, orderType } of orderGroups) {
|
|
340
346
|
const firstItem = group[0].item;
|
|
341
|
-
const
|
|
347
|
+
const firstProduct = group[0].product;
|
|
348
|
+
const groupTotal = group.reduce((sum, { item, product }) => sum + unitPriceFor(item, product) * item.quantity, 0);
|
|
342
349
|
total += groupTotal;
|
|
343
350
|
// S-SBUNI-RULES 2026-05-13 — order-item decoration via rule registry.
|
|
344
351
|
const orderItems = group.map(({ item, product }) => {
|
|
@@ -352,12 +359,13 @@ export async function createCheckoutOrderAction(input) {
|
|
|
352
359
|
bundleProductIds: item.bundleProductIds,
|
|
353
360
|
}
|
|
354
361
|
: {};
|
|
362
|
+
const unitPrice = unitPriceFor(item, product);
|
|
355
363
|
const baseLine = {
|
|
356
364
|
productId: item.productId,
|
|
357
365
|
productTitle: item.productTitle,
|
|
358
366
|
quantity: item.quantity,
|
|
359
|
-
unitPrice
|
|
360
|
-
totalPrice:
|
|
367
|
+
unitPrice,
|
|
368
|
+
totalPrice: unitPrice * item.quantity,
|
|
361
369
|
...bundleFields,
|
|
362
370
|
};
|
|
363
371
|
return itemRule.decorateOrderItem(baseLine, product);
|
|
@@ -437,7 +445,7 @@ export async function createCheckoutOrderAction(input) {
|
|
|
437
445
|
userName,
|
|
438
446
|
userEmail,
|
|
439
447
|
quantity: totalQuantity,
|
|
440
|
-
unitPrice: firstItem
|
|
448
|
+
unitPrice: unitPriceFor(firstItem, firstProduct),
|
|
441
449
|
totalPrice: orderTotal,
|
|
442
450
|
currency: firstItem.currency ?? getDefaultCurrency(),
|
|
443
451
|
storeId: firstItem.storeId || undefined,
|
|
@@ -21,10 +21,12 @@ interface ClassifiedDocLike {
|
|
|
21
21
|
}
|
|
22
22
|
export declare function renderClassifiedOg(doc: ClassifiedDocLike | null | undefined, opts: {
|
|
23
23
|
siteName: string;
|
|
24
|
+
baseUrl?: string;
|
|
24
25
|
}): ReactElement;
|
|
25
26
|
export declare function renderClassifiedOgImage(data: ClassifiedOgData, siteName: string): ReactElement;
|
|
26
27
|
/** Type-safe overload that accepts the full ProductDocument. */
|
|
27
28
|
export declare function renderClassifiedOgFromDoc(doc: ProductDocument | null | undefined, opts: {
|
|
28
29
|
siteName: string;
|
|
30
|
+
baseUrl?: string;
|
|
29
31
|
}): ReactElement;
|
|
30
32
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
2
3
|
export function renderClassifiedOg(doc, opts) {
|
|
3
4
|
const meta = doc?.classified;
|
|
4
5
|
const location = meta?.meetupArea
|
|
@@ -15,7 +16,7 @@ export function renderClassifiedOg(doc, opts) {
|
|
|
15
16
|
title: doc?.title ?? "Classified Listing",
|
|
16
17
|
priceLabel,
|
|
17
18
|
location,
|
|
18
|
-
imageUrl: doc?.mainImage || doc?.images?.[0] || null,
|
|
19
|
+
imageUrl: resolveOgImageUrl(doc?.mainImage || doc?.images?.[0] || null, opts.baseUrl),
|
|
19
20
|
}, opts.siteName);
|
|
20
21
|
}
|
|
21
22
|
export function renderClassifiedOgImage(data, siteName) {
|
|
@@ -18,10 +18,12 @@ interface DigitalCodeDocLike {
|
|
|
18
18
|
}
|
|
19
19
|
export declare function renderDigitalCodeOg(doc: DigitalCodeDocLike | null | undefined, opts: {
|
|
20
20
|
siteName: string;
|
|
21
|
+
baseUrl?: string;
|
|
21
22
|
}): ReactElement;
|
|
22
23
|
export declare function renderDigitalCodeOgImage(data: DigitalCodeOgData, siteName: string): ReactElement;
|
|
23
24
|
/** Type-safe overload that accepts the full ProductDocument. */
|
|
24
25
|
export declare function renderDigitalCodeOgFromDoc(doc: ProductDocument | null | undefined, opts: {
|
|
25
26
|
siteName: string;
|
|
27
|
+
baseUrl?: string;
|
|
26
28
|
}): ReactElement;
|
|
27
29
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
2
3
|
export function renderDigitalCodeOg(doc, opts) {
|
|
3
4
|
const priceLabel = doc?.price != null
|
|
4
5
|
? new Intl.NumberFormat("en-IN", {
|
|
@@ -16,7 +17,7 @@ export function renderDigitalCodeOg(doc, opts) {
|
|
|
16
17
|
title: doc?.title ?? "Digital Code",
|
|
17
18
|
priceLabel,
|
|
18
19
|
deliveryMethod,
|
|
19
|
-
imageUrl: doc?.mainImage || doc?.images?.[0] || null,
|
|
20
|
+
imageUrl: resolveOgImageUrl(doc?.mainImage || doc?.images?.[0] || null, opts.baseUrl),
|
|
20
21
|
}, opts.siteName);
|
|
21
22
|
}
|
|
22
23
|
export function renderDigitalCodeOgImage(data, siteName) {
|
|
@@ -21,6 +21,7 @@ export declare function renderEventOg(doc: EventDocLike | null | undefined, opts
|
|
|
21
21
|
siteName: string;
|
|
22
22
|
locale?: string;
|
|
23
23
|
typeLabels?: Record<string, string>;
|
|
24
|
+
baseUrl?: string;
|
|
24
25
|
}): ReactElement;
|
|
25
26
|
export declare function renderEventOgImage(data: EventOgData, siteName: string): ReactElement;
|
|
26
27
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
2
3
|
const EVENT_TYPE_LABEL = {
|
|
3
4
|
TOURNAMENT: "Tournament",
|
|
4
5
|
CONVENTION: "Convention",
|
|
@@ -15,11 +16,12 @@ export function renderEventOg(doc, opts) {
|
|
|
15
16
|
const locale = opts.locale ?? "en-IN";
|
|
16
17
|
const labels = { ...EVENT_TYPE_LABEL, ...(opts.typeLabels ?? {}) };
|
|
17
18
|
const eventType = doc?.type ?? null;
|
|
18
|
-
const
|
|
19
|
+
const rawCoverImageUrl = doc?.coverImageUrl ??
|
|
19
20
|
(typeof doc?.coverImage === "string"
|
|
20
21
|
? doc.coverImage
|
|
21
22
|
: doc?.coverImage?.url) ??
|
|
22
23
|
null;
|
|
24
|
+
const coverImageUrl = resolveOgImageUrl(rawCoverImageUrl, opts.baseUrl);
|
|
23
25
|
const startsAt = doc?.startsAt
|
|
24
26
|
? new Date(doc.startsAt).toLocaleDateString(locale, {
|
|
25
27
|
day: "numeric",
|
|
@@ -18,10 +18,12 @@ interface LiveItemDocLike {
|
|
|
18
18
|
}
|
|
19
19
|
export declare function renderLiveItemOg(doc: LiveItemDocLike | null | undefined, opts: {
|
|
20
20
|
siteName: string;
|
|
21
|
+
baseUrl?: string;
|
|
21
22
|
}): ReactElement;
|
|
22
23
|
export declare function renderLiveItemOgImage(data: LiveItemOgData, siteName: string): ReactElement;
|
|
23
24
|
/** Type-safe overload that accepts the full ProductDocument. */
|
|
24
25
|
export declare function renderLiveItemOgFromDoc(doc: ProductDocument | null | undefined, opts: {
|
|
25
26
|
siteName: string;
|
|
27
|
+
baseUrl?: string;
|
|
26
28
|
}): ReactElement;
|
|
27
29
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
2
3
|
export function renderLiveItemOg(doc, opts) {
|
|
3
4
|
const priceLabel = doc?.price != null
|
|
4
5
|
? new Intl.NumberFormat("en-IN", {
|
|
@@ -11,7 +12,7 @@ export function renderLiveItemOg(doc, opts) {
|
|
|
11
12
|
title: doc?.title ?? "Live Listing",
|
|
12
13
|
species: doc?.liveItem?.species ?? null,
|
|
13
14
|
priceLabel,
|
|
14
|
-
imageUrl: doc?.mainImage || doc?.images?.[0] || null,
|
|
15
|
+
imageUrl: resolveOgImageUrl(doc?.mainImage || doc?.images?.[0] || null, opts.baseUrl),
|
|
15
16
|
}, opts.siteName);
|
|
16
17
|
}
|
|
17
18
|
export function renderLiveItemOgImage(data, siteName) {
|
|
@@ -14,6 +14,7 @@ interface PreOrderDocLike {
|
|
|
14
14
|
export declare function renderPreOrderOg(doc: PreOrderDocLike | null | undefined, opts: {
|
|
15
15
|
siteName: string;
|
|
16
16
|
locale?: string;
|
|
17
|
+
baseUrl?: string;
|
|
17
18
|
}): ReactElement;
|
|
18
19
|
export declare function renderPreOrderOgImage(data: PreOrderOgData, siteName: string): ReactElement;
|
|
19
20
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
2
3
|
/** High-level OG renderer — accepts the raw pre-order document from `getPreOrderForDetail`. */
|
|
3
4
|
export function renderPreOrderOg(doc, opts) {
|
|
4
5
|
const locale = opts.locale ?? "en-IN";
|
|
@@ -11,7 +12,7 @@ export function renderPreOrderOg(doc, opts) {
|
|
|
11
12
|
return renderPreOrderOgImage({
|
|
12
13
|
title: doc?.title ?? "Pre-Order",
|
|
13
14
|
releaseDateLabel,
|
|
14
|
-
imageUrl: doc?.mainImage || doc?.images?.[0] || null,
|
|
15
|
+
imageUrl: resolveOgImageUrl(doc?.mainImage || doc?.images?.[0] || null, opts.baseUrl),
|
|
15
16
|
}, opts.siteName);
|
|
16
17
|
}
|
|
17
18
|
export function renderPreOrderOgImage(data, siteName) {
|
|
@@ -14,6 +14,8 @@ interface ProductDocLike {
|
|
|
14
14
|
images?: (string | null | undefined)[] | null;
|
|
15
15
|
}
|
|
16
16
|
/** High-level OG renderer — accepts the raw product document from `getProductForDetail`. */
|
|
17
|
-
export declare function renderProductOg(doc: ProductDocLike | null | undefined, opts: OgOptions
|
|
17
|
+
export declare function renderProductOg(doc: ProductDocLike | null | undefined, opts: OgOptions & {
|
|
18
|
+
baseUrl?: string;
|
|
19
|
+
}): ReactElement;
|
|
18
20
|
export declare function renderProductOgImage(data: ProductOgData, siteName: string): ReactElement;
|
|
19
21
|
export {};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
2
3
|
/** High-level OG renderer — accepts the raw product document from `getProductForDetail`. */
|
|
3
4
|
export function renderProductOg(doc, opts) {
|
|
4
5
|
return renderProductOgImage({
|
|
5
6
|
title: doc?.title ?? "Product",
|
|
6
7
|
price: doc?.price ?? null,
|
|
7
|
-
imageUrl: doc?.mainImage || doc?.images?.[0] || null,
|
|
8
|
+
imageUrl: resolveOgImageUrl(doc?.mainImage || doc?.images?.[0] || null, opts.baseUrl),
|
|
8
9
|
}, opts.siteName);
|
|
9
10
|
}
|
|
10
11
|
export function renderProductOgImage(data, siteName) {
|
|
@@ -4,5 +4,5 @@ export { buildManifest } from "./manifest";
|
|
|
4
4
|
export type { ManifestOptions } from "./manifest";
|
|
5
5
|
export { buildSitemap } from "./sitemap";
|
|
6
6
|
export type { SitemapOptions } from "./sitemap";
|
|
7
|
-
export { buildDefaultOgImage, DEFAULT_OG_SIZE } from "./og";
|
|
7
|
+
export { buildDefaultOgImage, DEFAULT_OG_SIZE, resolveOgImageUrl } from "./og";
|
|
8
8
|
export type { DefaultOgOptions } from "./og";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { buildRobots } from "./robots";
|
|
2
2
|
export { buildManifest } from "./manifest";
|
|
3
3
|
export { buildSitemap } from "./sitemap";
|
|
4
|
-
export { buildDefaultOgImage, DEFAULT_OG_SIZE } from "./og";
|
|
4
|
+
export { buildDefaultOgImage, DEFAULT_OG_SIZE, resolveOgImageUrl } from "./og";
|
|
@@ -3,9 +3,16 @@ export declare const DEFAULT_OG_SIZE: {
|
|
|
3
3
|
width: number;
|
|
4
4
|
height: number;
|
|
5
5
|
};
|
|
6
|
+
/**
|
|
7
|
+
* Resolves a potentially relative image URL to an absolute URL for use in
|
|
8
|
+
* next/og (Satori). Satori cannot resolve relative paths; all <img src> values
|
|
9
|
+
* must be absolute URLs or data URIs.
|
|
10
|
+
*/
|
|
11
|
+
export declare function resolveOgImageUrl(url: string | null | undefined, baseUrl?: string): string | null;
|
|
6
12
|
export interface DefaultOgOptions {
|
|
7
13
|
siteName: string;
|
|
8
14
|
tagline?: string;
|
|
9
15
|
domain?: string;
|
|
16
|
+
logoUrl?: string;
|
|
10
17
|
}
|
|
11
|
-
export declare function buildDefaultOgImage({ siteName, tagline, domain }: DefaultOgOptions): ImageResponse;
|
|
18
|
+
export declare function buildDefaultOgImage({ siteName, tagline, domain, logoUrl }: DefaultOgOptions): ImageResponse;
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ImageResponse } from "next/og";
|
|
3
3
|
export const DEFAULT_OG_SIZE = { width: 1200, height: 630 };
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a potentially relative image URL to an absolute URL for use in
|
|
6
|
+
* next/og (Satori). Satori cannot resolve relative paths; all <img src> values
|
|
7
|
+
* must be absolute URLs or data URIs.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveOgImageUrl(url, baseUrl) {
|
|
10
|
+
if (!url)
|
|
11
|
+
return null;
|
|
12
|
+
if (!baseUrl)
|
|
13
|
+
return url;
|
|
14
|
+
if (url.startsWith("http://") ||
|
|
15
|
+
url.startsWith("https://") ||
|
|
16
|
+
url.startsWith("data:")) {
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
return `${baseUrl.replace(/\/$/, "")}${url.startsWith("/") ? "" : "/"}${url}`;
|
|
20
|
+
}
|
|
21
|
+
export function buildDefaultOgImage({ siteName, tagline, domain, logoUrl }) {
|
|
5
22
|
const subtitle = tagline ?? "Shop, Bid & Sell — India's Multi-Seller Marketplace";
|
|
6
23
|
return new ImageResponse(_jsxs("div", { style: {
|
|
7
24
|
width: "100%",
|
|
@@ -24,29 +41,32 @@ export function buildDefaultOgImage({ siteName, tagline, domain }) {
|
|
|
24
41
|
display: "flex",
|
|
25
42
|
alignItems: "center",
|
|
26
43
|
justifyContent: "center",
|
|
27
|
-
width:
|
|
28
|
-
height:
|
|
29
|
-
borderRadius:
|
|
30
|
-
background: "rgba(255,255,255,0.
|
|
31
|
-
marginBottom:
|
|
32
|
-
|
|
33
|
-
}, children: "\uD83D\uDECD\uFE0F" }), _jsx("div", { style: {
|
|
44
|
+
width: 140,
|
|
45
|
+
height: 140,
|
|
46
|
+
borderRadius: 28,
|
|
47
|
+
background: "rgba(255,255,255,0.18)",
|
|
48
|
+
marginBottom: 36,
|
|
49
|
+
overflow: "hidden",
|
|
50
|
+
}, children: logoUrl ? (_jsx("img", { src: logoUrl, alt: siteName, style: { width: 100, height: 100, objectFit: "contain" } })) : (_jsx("div", { style: { fontSize: 64, display: "flex" }, children: "\uD83D\uDECD\uFE0F" })) }), _jsx("div", { style: {
|
|
34
51
|
fontSize: 80,
|
|
35
52
|
fontWeight: 800,
|
|
36
53
|
color: "white",
|
|
37
54
|
letterSpacing: "-2px",
|
|
38
55
|
marginBottom: 16,
|
|
39
56
|
textShadow: "0 4px 24px rgba(0,0,0,0.3)",
|
|
57
|
+
display: "flex",
|
|
40
58
|
}, children: siteName }), _jsx("div", { style: {
|
|
41
59
|
fontSize: 32,
|
|
42
60
|
color: "rgba(255,255,255,0.85)",
|
|
43
61
|
fontWeight: 400,
|
|
44
62
|
letterSpacing: "0.5px",
|
|
63
|
+
display: "flex",
|
|
45
64
|
}, children: subtitle }), domain && (_jsx("div", { style: {
|
|
46
65
|
position: "absolute",
|
|
47
66
|
bottom: 36,
|
|
48
67
|
fontSize: 22,
|
|
49
68
|
color: "rgba(255,255,255,0.6)",
|
|
50
69
|
letterSpacing: "1px",
|
|
70
|
+
display: "flex",
|
|
51
71
|
}, children: domain }))] }), { ...DEFAULT_OG_SIZE });
|
|
52
72
|
}
|
|
@@ -14,6 +14,7 @@ interface StoreDocLike {
|
|
|
14
14
|
/** High-level OG renderer — accepts the raw store document from `getStoreForDetail`. */
|
|
15
15
|
export declare function renderStoreOg(doc: StoreDocLike | null | undefined, opts: {
|
|
16
16
|
siteName: string;
|
|
17
|
+
baseUrl?: string;
|
|
17
18
|
}): ReactElement;
|
|
18
19
|
export declare function renderStoreOgImage(data: StoreOgData, siteName: string): ReactElement;
|
|
19
20
|
export {};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
2
3
|
/** High-level OG renderer — accepts the raw store document from `getStoreForDetail`. */
|
|
3
4
|
export function renderStoreOg(doc, opts) {
|
|
4
5
|
return renderStoreOgImage({
|
|
5
6
|
name: doc?.storeName ?? `${opts.siteName} Store`,
|
|
6
7
|
description: doc?.storeDescription?.slice(0, 120) ?? null,
|
|
7
|
-
logoUrl: doc?.storeLogoURL ?? null,
|
|
8
|
-
bannerUrl: doc?.storeBannerURL ?? null,
|
|
8
|
+
logoUrl: resolveOgImageUrl(doc?.storeLogoURL ?? null, opts.baseUrl),
|
|
9
|
+
bannerUrl: resolveOgImageUrl(doc?.storeBannerURL ?? null, opts.baseUrl),
|
|
9
10
|
}, opts.siteName);
|
|
10
11
|
}
|
|
11
12
|
export function renderStoreOgImage(data, siteName) {
|
|
@@ -15,6 +15,7 @@ interface SublistingCategoryDocLike {
|
|
|
15
15
|
/** High-level OG renderer — accepts the raw sublisting-category doc from the repository. */
|
|
16
16
|
export declare function renderSublistingCategoryOg(doc: SublistingCategoryDocLike | null | undefined, opts: {
|
|
17
17
|
siteName: string;
|
|
18
|
+
baseUrl?: string;
|
|
18
19
|
}): ReactElement;
|
|
19
20
|
export declare function renderSublistingCategoryOgImage(data: SublistingCategoryOgData, siteName: string): ReactElement;
|
|
20
21
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
2
3
|
/** High-level OG renderer — accepts the raw sublisting-category doc from the repository. */
|
|
3
4
|
export function renderSublistingCategoryOg(doc, opts) {
|
|
4
5
|
const baseName = doc?.name ?? "Sub-listing";
|
|
@@ -7,7 +8,7 @@ export function renderSublistingCategoryOg(doc, opts) {
|
|
|
7
8
|
name,
|
|
8
9
|
description: doc?.description?.slice(0, 120) ?? null,
|
|
9
10
|
productCount: doc?.productCount ?? null,
|
|
10
|
-
coverImage: doc?.coverImage ?? null,
|
|
11
|
+
coverImage: resolveOgImageUrl(doc?.coverImage ?? null, opts.baseUrl),
|
|
11
12
|
}, opts.siteName);
|
|
12
13
|
}
|
|
13
14
|
export function renderSublistingCategoryOgImage(data, siteName) {
|
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { userRepository } from "../../../../repositories";
|
|
2
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
2
3
|
import { SCAM_TYPE_LABELS } from "../../../../features/scams/constants/scam-types";
|
|
3
4
|
export async function handleScamReportCreate(input, ctx) {
|
|
4
5
|
const { scammerId, report } = input;
|
|
5
6
|
const { reportedBy, displayNames, scamType, scamPlatform, amountLost } = report;
|
|
6
7
|
const name = displayNames?.[0] ?? "Unknown";
|
|
7
|
-
// 1. Notify the reporter
|
|
8
|
+
// 1. Notify the reporter (multi-channel: respects user notification prefs)
|
|
8
9
|
if (reportedBy) {
|
|
9
10
|
try {
|
|
10
|
-
await
|
|
11
|
+
const reporter = await userRepository.findById(reportedBy);
|
|
12
|
+
await sendNotification({
|
|
11
13
|
userId: reportedBy,
|
|
12
14
|
type: "account_action",
|
|
15
|
+
priority: "normal",
|
|
13
16
|
title: "Scam report submitted",
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
message: `Your report for "${name}" has been received. Our team will review it within 48 hours.`,
|
|
18
|
+
relatedId: scammerId,
|
|
19
|
+
relatedType: "scammer",
|
|
20
|
+
userEmail: reporter?.email ?? undefined,
|
|
21
|
+
userPhone: reporter?.phoneNumber ?? undefined,
|
|
19
22
|
});
|
|
20
23
|
}
|
|
21
24
|
catch (err) {
|
|
@@ -32,15 +35,16 @@ export async function handleScamReportCreate(input, ctx) {
|
|
|
32
35
|
const scamTypeLabel = scamType ? (SCAM_TYPE_LABELS[scamType] ?? scamType) : "Unknown";
|
|
33
36
|
const amountStr = amountLost ? ` ₹${(amountLost / 100).toLocaleString("en-IN")}` : "";
|
|
34
37
|
const platformStr = scamPlatform ? ` via ${scamPlatform}` : "";
|
|
35
|
-
await Promise.all(result.items.map((employee) =>
|
|
38
|
+
await Promise.all(result.items.filter((e) => !!e.id).map((employee) => sendNotification({
|
|
36
39
|
userId: employee.id,
|
|
37
40
|
type: "account_action",
|
|
41
|
+
priority: "normal",
|
|
38
42
|
title: "New scam report submitted",
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
message: `A report was submitted for "${name}" — ${scamTypeLabel}${platformStr}.${amountStr}`,
|
|
44
|
+
relatedId: scammerId,
|
|
45
|
+
relatedType: "scammer",
|
|
46
|
+
userEmail: employee.email ?? undefined,
|
|
47
|
+
userPhone: employee.phoneNumber ?? undefined,
|
|
44
48
|
}).catch((err) => ctx.logger.error("Failed to notify employee (non-fatal)", err, { scammerId, employeeId: employee.id }))));
|
|
45
49
|
}
|
|
46
50
|
catch (err) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { userRepository } from "../../../../repositories";
|
|
2
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
2
3
|
export async function handleScamReportRejected(input, ctx) {
|
|
3
4
|
const { scammerId, report } = input;
|
|
4
5
|
const { reportedBy, displayNames, prevStatus, nextStatus } = report;
|
|
@@ -10,15 +11,17 @@ export async function handleScamReportRejected(input, ctx) {
|
|
|
10
11
|
return;
|
|
11
12
|
const name = displayNames?.[0] ?? "Unknown";
|
|
12
13
|
try {
|
|
13
|
-
await
|
|
14
|
+
const reporter = await userRepository.findById(reportedBy);
|
|
15
|
+
await sendNotification({
|
|
14
16
|
userId: reportedBy,
|
|
15
17
|
type: "account_action",
|
|
18
|
+
priority: "normal",
|
|
16
19
|
title: "Scam report not verified",
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
message: `Your report for "${name}" could not be verified with the evidence provided. You may submit a new report with additional evidence.`,
|
|
21
|
+
relatedId: scammerId,
|
|
22
|
+
relatedType: "scammer",
|
|
23
|
+
userEmail: reporter?.email ?? undefined,
|
|
24
|
+
userPhone: reporter?.phoneNumber ?? undefined,
|
|
22
25
|
});
|
|
23
26
|
}
|
|
24
27
|
catch (err) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { userRepository } from "../../../../repositories";
|
|
2
|
+
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
2
3
|
export async function handleScamReportVerified(input, ctx) {
|
|
3
4
|
const { scammerId, report } = input;
|
|
4
5
|
const { reportedBy, displayNames, prevStatus, nextStatus } = report;
|
|
@@ -10,15 +11,17 @@ export async function handleScamReportVerified(input, ctx) {
|
|
|
10
11
|
return;
|
|
11
12
|
const name = displayNames?.[0] ?? "Unknown";
|
|
12
13
|
try {
|
|
13
|
-
await
|
|
14
|
+
const reporter = await userRepository.findById(reportedBy);
|
|
15
|
+
await sendNotification({
|
|
14
16
|
userId: reportedBy,
|
|
15
17
|
type: "account_action",
|
|
18
|
+
priority: "normal",
|
|
16
19
|
title: "Your scam report was verified",
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
message: `The report for "${name}" has been verified and published to the Scam Registry. Thank you for helping protect the community.`,
|
|
21
|
+
relatedId: scammerId,
|
|
22
|
+
relatedType: "scammer",
|
|
23
|
+
userEmail: reporter?.email ?? undefined,
|
|
24
|
+
userPhone: reporter?.phoneNumber ?? undefined,
|
|
22
25
|
});
|
|
23
26
|
}
|
|
24
27
|
catch (err) {
|
package/dist/client.d.ts
CHANGED
|
@@ -61,6 +61,8 @@ export { Select } from "./ui/components/Select";
|
|
|
61
61
|
export type { SelectOption, SelectProps } from "./ui/components/Select";
|
|
62
62
|
export { Heading } from "./ui/components/Typography";
|
|
63
63
|
export { Label, Text } from "./ui/components/Typography";
|
|
64
|
+
export { TextLink } from "./ui/components/TextLink";
|
|
65
|
+
export type { TextLinkProps } from "./ui/components/TextLink";
|
|
64
66
|
export { Textarea } from "./ui/components/Textarea";
|
|
65
67
|
export { GlobalError } from "./next/components/GlobalError";
|
|
66
68
|
export { AppLayoutShell, LocaleSwitcher, useDashboardNav, BottomActionsProvider, DashboardNavProvider, LayoutClient, ListingLayout } from "./features/layout/index";
|
package/dist/client.js
CHANGED
|
@@ -126,6 +126,7 @@ export { DateInput, DateRangeInput } from "./ui/components/DateInput";
|
|
|
126
126
|
export { Select } from "./ui/components/Select";
|
|
127
127
|
export { Heading } from "./ui/components/Typography";
|
|
128
128
|
export { Label, Text } from "./ui/components/Typography";
|
|
129
|
+
export { TextLink } from "./ui/components/TextLink";
|
|
129
130
|
export { Textarea } from "./ui/components/Textarea";
|
|
130
131
|
export { GlobalError } from "./next/components/GlobalError";
|
|
131
132
|
export { AppLayoutShell, LocaleSwitcher, useDashboardNav, BottomActionsProvider, DashboardNavProvider, LayoutClient, ListingLayout } from "./features/layout/index";
|
|
@@ -71,6 +71,9 @@ export declare const ADMIN_ENDPOINTS: {
|
|
|
71
71
|
readonly BID_BY_ID: (id: string) => string;
|
|
72
72
|
readonly BLOG: "/api/admin/blog";
|
|
73
73
|
readonly BLOG_BY_ID: (id: string) => string;
|
|
74
|
+
readonly BUNDLES: "/api/admin/bundles";
|
|
75
|
+
readonly BUNDLE_BY_ID: (id: string) => string;
|
|
76
|
+
readonly BUNDLE_REBUILD: (id: string) => string;
|
|
74
77
|
readonly CATEGORIES: "/api/admin/categories";
|
|
75
78
|
readonly CATEGORY_BY_ID: (id: string) => string;
|
|
76
79
|
readonly BRANDS: "/api/admin/brands";
|
|
@@ -371,6 +374,9 @@ export declare const API_ENDPOINTS: {
|
|
|
371
374
|
readonly BID_BY_ID: (id: string) => string;
|
|
372
375
|
readonly BLOG: "/api/admin/blog";
|
|
373
376
|
readonly BLOG_BY_ID: (id: string) => string;
|
|
377
|
+
readonly BUNDLES: "/api/admin/bundles";
|
|
378
|
+
readonly BUNDLE_BY_ID: (id: string) => string;
|
|
379
|
+
readonly BUNDLE_REBUILD: (id: string) => string;
|
|
374
380
|
readonly CATEGORIES: "/api/admin/categories";
|
|
375
381
|
readonly CATEGORY_BY_ID: (id: string) => string;
|
|
376
382
|
readonly BRANDS: "/api/admin/brands";
|
|
@@ -673,6 +679,9 @@ export declare const API_ROUTES: {
|
|
|
673
679
|
readonly BID_BY_ID: (id: string) => string;
|
|
674
680
|
readonly BLOG: "/api/admin/blog";
|
|
675
681
|
readonly BLOG_BY_ID: (id: string) => string;
|
|
682
|
+
readonly BUNDLES: "/api/admin/bundles";
|
|
683
|
+
readonly BUNDLE_BY_ID: (id: string) => string;
|
|
684
|
+
readonly BUNDLE_REBUILD: (id: string) => string;
|
|
676
685
|
readonly CATEGORIES: "/api/admin/categories";
|
|
677
686
|
readonly CATEGORY_BY_ID: (id: string) => string;
|
|
678
687
|
readonly BRANDS: "/api/admin/brands";
|
|
@@ -89,6 +89,9 @@ export const ADMIN_ENDPOINTS = {
|
|
|
89
89
|
BID_BY_ID: (id) => `/api/admin/bids/${id}`,
|
|
90
90
|
BLOG: "/api/admin/blog",
|
|
91
91
|
BLOG_BY_ID: (id) => `/api/admin/blog/${id}`,
|
|
92
|
+
BUNDLES: "/api/admin/bundles",
|
|
93
|
+
BUNDLE_BY_ID: (id) => `/api/admin/bundles/${id}`,
|
|
94
|
+
BUNDLE_REBUILD: (id) => `/api/admin/bundles/${id}/rebuild`,
|
|
92
95
|
CATEGORIES: "/api/admin/categories",
|
|
93
96
|
CATEGORY_BY_ID: (id) => `/api/admin/categories/${id}`,
|
|
94
97
|
BRANDS: "/api/admin/brands",
|
|
@@ -13,8 +13,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
13
13
|
*/
|
|
14
14
|
import { useCallback, useEffect, useState } from "react";
|
|
15
15
|
import Link from "next/link";
|
|
16
|
-
import { Badge, Button, Container, Div, Heading, Row, Section, Stack, Text, } from "../../../ui";
|
|
16
|
+
import { Badge, Button, Container, Div, Heading, Row, Section, Stack, Text, useToast, } from "../../../ui";
|
|
17
17
|
import { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "../../../_internal/shared/features/categories/bundle-copy";
|
|
18
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
18
19
|
const STOCK_LIST_LABEL = {
|
|
19
20
|
in_stock: BUNDLE_COPY.stockBadge.listVariantInStock,
|
|
20
21
|
out_of_stock: BUNDLE_COPY.stockBadge.listVariantOutOfStock,
|
|
@@ -28,6 +29,23 @@ export function AdminBundlesView({ getEditHref, newHref, }) {
|
|
|
28
29
|
const [bundles, setBundles] = useState([]);
|
|
29
30
|
const [loading, setLoading] = useState(true);
|
|
30
31
|
const [error, setError] = useState(null);
|
|
32
|
+
const [rebuildingId, setRebuildingId] = useState(null);
|
|
33
|
+
const toast = useToast();
|
|
34
|
+
const handleRebuild = useCallback(async (bundleId) => {
|
|
35
|
+
setRebuildingId(bundleId);
|
|
36
|
+
try {
|
|
37
|
+
const res = await fetch(`/api/admin/bundles/${encodeURIComponent(bundleId)}/rebuild`, { method: "POST" });
|
|
38
|
+
if (!res.ok)
|
|
39
|
+
throw new Error("Rebuild failed");
|
|
40
|
+
toast.showToast("Bundle stock rebuilt.", "success");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
toast.showToast("Failed to rebuild bundle stock.", "error");
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
setRebuildingId(null);
|
|
47
|
+
}
|
|
48
|
+
}, [toast]);
|
|
31
49
|
const load = useCallback(async () => {
|
|
32
50
|
setLoading(true);
|
|
33
51
|
setError(null);
|
|
@@ -53,6 +71,6 @@ export function AdminBundlesView({ getEditHref, newHref, }) {
|
|
|
53
71
|
const memberCount = b.bundleProductIds?.length ?? 0;
|
|
54
72
|
return (_jsxs("tr", { className: "border-t border-zinc-100 dark:border-zinc-800", children: [_jsx("td", { className: "px-3 py-2", children: _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "sm", weight: "medium", children: b.name }), _jsx(Text, { size: "xs", color: "muted", children: b.slug })] }) }), _jsx("td", { className: "px-3 py-2", children: formatPrice(b.bundlePriceInPaise) }), _jsx("td", { className: "px-3 py-2", children: memberCount }), _jsx("td", { className: "px-3 py-2", children: _jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[stockKey], children: STOCK_LIST_LABEL[stockKey] }) }), _jsx("td", { className: "px-3 py-2", children: _jsx(Badge, { variant: b.isActive ? "success" : "default", children: b.isActive
|
|
55
73
|
? BUNDLE_COPY.adminList.activeBadge
|
|
56
|
-
: BUNDLE_COPY.adminList.inactiveBadge }) }), _jsx("td", { className: "px-3 py-2 text-right", children: _jsx(Button, { asChild: true, variant: "ghost", size: "sm", children: _jsx(Link, { href: getEditHref({ id: b.id }), children: BUNDLE_COPY.adminList.editLabel }) }) })] }, b.id));
|
|
74
|
+
: BUNDLE_COPY.adminList.inactiveBadge }) }), _jsx("td", { className: "px-3 py-2 text-right", children: _jsxs(Row, { gap: "xs", justify: "end", children: [_jsx(Button, { variant: "ghost", size: "sm", isLoading: rebuildingId === b.id, disabled: rebuildingId === b.id, onClick: () => handleRebuild(b.id), children: ACTIONS.ADMIN["rebuild-bundle"].label }), _jsx(Button, { asChild: true, variant: "ghost", size: "sm", children: _jsx(Link, { href: getEditHref({ id: b.id }), children: BUNDLE_COPY.adminList.editLabel }) })] }) })] }, b.id));
|
|
57
75
|
}) })] }) }))] }) }) }));
|
|
58
76
|
}
|
|
@@ -8,6 +8,7 @@ import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
|
|
|
8
8
|
import { BulkActionBar, Button, Form, FormActions, FilterChipGroup, Input, ListingToolbar, ListingViewShell, Modal, Pagination, RowActionMenu, useToast, } from "../../../ui";
|
|
9
9
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
10
|
import { ADMIN_PAYOUT_STATUS_TABS } from "../constants/filter-tabs";
|
|
11
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
11
12
|
import { toRecordArray, toRelativeDate, toRupees, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
|
|
12
13
|
import { DataTable } from "./DataTable";
|
|
13
14
|
import { apiClient } from "../../../http";
|
|
@@ -135,7 +136,7 @@ export function AdminPayoutsView({ children, ...props }) {
|
|
|
135
136
|
const pr = row;
|
|
136
137
|
return (_jsx(RowActionMenu, { actions: [
|
|
137
138
|
{
|
|
138
|
-
label: "
|
|
139
|
+
label: ACTIONS.ADMIN["grant-payout"].label,
|
|
139
140
|
onClick: () => {
|
|
140
141
|
setSelectedPayoutId(pr.id);
|
|
141
142
|
setMarkPaidOpen(true);
|
|
@@ -12,6 +12,7 @@ import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } fr
|
|
|
12
12
|
import { DataTable } from "./DataTable";
|
|
13
13
|
import { AdminViewCards } from "./AdminViewCards";
|
|
14
14
|
import { apiClient } from "../../../http";
|
|
15
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
15
16
|
import { AdminProductEditorView } from "./AdminProductEditorView";
|
|
16
17
|
import { QuickEditMenu } from "./QuickEditMenu";
|
|
17
18
|
const PAGE_SIZE = 25;
|
|
@@ -183,8 +184,20 @@ export function AdminProductsView({ children, actionHref, getRowHref, ...props }
|
|
|
183
184
|
{ id: "sale", label: "Toggle On Sale", variant: "secondary", onClick: () => { for (const id of selection.selectedIds)
|
|
184
185
|
void handleToggle(id, "isOnSale", !rows.find(r => r.id === id)?.isOnSale); selection.clearSelection(); } },
|
|
185
186
|
] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, columns: [...buildBaseColumns(), flagColumn], isLoading: isLoading, emptyLabel: "No products found", onRowClick: (row) => openEditPanel(row.id), selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), renderRowActions: (row) => (_jsx(QuickEditMenu, { actions: [
|
|
187
|
+
{
|
|
188
|
+
label: ACTIONS.ADMIN["approve-product"].label,
|
|
189
|
+
onClick: () => handleQuickEdit(row.id, { status: "published" }),
|
|
190
|
+
disabled: row.status === "published",
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
label: ACTIONS.ADMIN["reject-product"].label,
|
|
194
|
+
destructive: true,
|
|
195
|
+
onClick: () => handleQuickEdit(row.id, { status: "rejected" }),
|
|
196
|
+
disabled: row.status === "rejected",
|
|
197
|
+
},
|
|
186
198
|
{
|
|
187
199
|
label: "Quick edit",
|
|
200
|
+
separator: true,
|
|
188
201
|
formTitle: "Quick Edit Product",
|
|
189
202
|
fields: [
|
|
190
203
|
{ name: "status", label: "Status", type: "select", required: true,
|
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import React, { useState, useCallback } from "react";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
5
6
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
7
|
import { usePanelUrlSync } from "../../../react/hooks/use-panel-url-sync";
|
|
7
8
|
import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
|
|
8
|
-
import { BulkActionBar, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, RowActionMenu } from "../../../ui";
|
|
9
|
+
import { BulkActionBar, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, RowActionMenu, useToast } from "../../../ui";
|
|
9
10
|
import { AdminViewCards } from "./AdminViewCards";
|
|
10
11
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
12
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
11
13
|
import { ADMIN_STORE_STATUS_TABS } from "../constants/filter-tabs";
|
|
12
14
|
import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
|
|
15
|
+
import { apiClient } from "../../../http";
|
|
13
16
|
import { DataTable } from "./DataTable";
|
|
14
17
|
import { AdminStoreEditorView } from "./AdminStoreEditorView";
|
|
15
18
|
const PAGE_SIZE = 25;
|
|
@@ -29,8 +32,26 @@ function StoresFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clea
|
|
|
29
32
|
export function AdminStoresView({ children, ...props }) {
|
|
30
33
|
const hasChildren = React.Children.count(children) > 0;
|
|
31
34
|
const [view, setView] = useState("table");
|
|
35
|
+
const toast = useToast();
|
|
36
|
+
const queryClient = useQueryClient();
|
|
32
37
|
const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
|
|
33
38
|
const { openEditPanel, closePanel, isEditOpen, editId } = usePanelUrlSync();
|
|
39
|
+
const verifyStore = useMutation({
|
|
40
|
+
mutationFn: (storeId) => apiClient.patch(ADMIN_ENDPOINTS.STORE_BY_ID(storeId), { isVerified: true }),
|
|
41
|
+
onSuccess: () => {
|
|
42
|
+
toast.showToast("Store verified.", "success");
|
|
43
|
+
void queryClient.invalidateQueries({ queryKey: ["admin", "stores", "listing"] });
|
|
44
|
+
},
|
|
45
|
+
onError: () => { toast.showToast("Failed to verify store.", "error"); },
|
|
46
|
+
});
|
|
47
|
+
const suspendStore = useMutation({
|
|
48
|
+
mutationFn: (storeId) => apiClient.patch(ADMIN_ENDPOINTS.STORE_BY_ID(storeId), { storeStatus: "suspended" }),
|
|
49
|
+
onSuccess: () => {
|
|
50
|
+
toast.showToast("Store suspended.", "success");
|
|
51
|
+
void queryClient.invalidateQueries({ queryKey: ["admin", "stores", "listing"] });
|
|
52
|
+
},
|
|
53
|
+
onError: () => { toast.showToast("Failed to suspend store.", "error"); },
|
|
54
|
+
});
|
|
34
55
|
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
35
56
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
36
57
|
const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
|
|
@@ -93,10 +114,27 @@ export function AdminStoresView({ children, ...props }) {
|
|
|
93
114
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search stores, slugs, or owner names", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
94
115
|
{ id: "manage", label: "Manage Store", variant: "primary", onClick: () => { const id = selection.selectedIds[0]; if (id)
|
|
95
116
|
openEditPanel(id); selection.clearSelection(); } },
|
|
96
|
-
] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No stores found", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), renderRowActions: (row) =>
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
117
|
+
] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No stores found", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), renderRowActions: (row) => {
|
|
118
|
+
const sr = row;
|
|
119
|
+
const isSuspended = sr.status?.toLowerCase() === "suspended";
|
|
120
|
+
const isVerified = Boolean(sr._raw?.isVerified);
|
|
121
|
+
return (_jsx(RowActionMenu, { actions: [
|
|
122
|
+
{
|
|
123
|
+
label: "Manage",
|
|
124
|
+
onClick: () => openEditPanel(sr.id),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
label: ACTIONS.ADMIN["verify-store"].label,
|
|
128
|
+
onClick: () => verifyStore.mutate(sr.id),
|
|
129
|
+
disabled: isVerified || verifyStore.isPending,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
label: ACTIONS.ADMIN["suspend-store"].label,
|
|
133
|
+
onClick: () => suspendStore.mutate(sr.id),
|
|
134
|
+
disabled: isSuspended || suspendStore.isPending,
|
|
135
|
+
},
|
|
136
|
+
] }));
|
|
137
|
+
} })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No stores found", onRowClick: (row) => openEditPanel(row.id), selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), _jsx(StoresFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters })] }), _jsx(AdminStoreEditorView, { open: isEditOpen, onClose: closePanel, storeId: editId ?? undefined, storeName: panelRow?.primary, currentStatus: panelRow?.status?.toLowerCase(), currentIsVerified: Boolean(panelRow?._raw?.isVerified), currentCapabilities: Array.isArray(panelRow?._raw?.capabilities)
|
|
100
138
|
? panelRow._raw.capabilities
|
|
101
139
|
: undefined })] }));
|
|
102
140
|
}
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import React, { useState, useCallback } from "react";
|
|
4
4
|
import { X } from "lucide-react";
|
|
5
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
5
6
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
7
|
import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
|
|
7
|
-
import { BulkActionBar, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, RowActionMenu } from "../../../ui";
|
|
8
|
+
import { BulkActionBar, Button, FilterChipGroup, Form, FormActions, Input, ListingToolbar, Modal, Pagination, ListingViewShell, RowActionMenu, Text as AppText, useToast } from "../../../ui";
|
|
8
9
|
import { AdminViewCards } from "./AdminViewCards";
|
|
9
10
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
11
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
10
12
|
import { ADMIN_USER_STATUS_TABS, ADMIN_USER_ROLE_TABS } from "../constants/filter-tabs";
|
|
11
13
|
import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
|
|
14
|
+
import { apiClient } from "../../../http";
|
|
12
15
|
import { DataTable } from "./DataTable";
|
|
13
16
|
import { AdminUserEditorView } from "./AdminUserEditorView";
|
|
14
17
|
const PAGE_SIZE = 25;
|
|
@@ -29,12 +32,40 @@ function UsersFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clear
|
|
|
29
32
|
export function AdminUsersView({ children, ...props }) {
|
|
30
33
|
const hasChildren = React.Children.count(children) > 0;
|
|
31
34
|
const [view, setView] = useState("table");
|
|
35
|
+
const toast = useToast();
|
|
36
|
+
const queryClient = useQueryClient();
|
|
32
37
|
const table = useUrlTable({ defaults: { pageSize: String(PAGE_SIZE), sort: DEFAULT_SORT } });
|
|
33
38
|
const [searchInput, setSearchInput] = useState(table.get("q") || "");
|
|
34
39
|
const [filterOpen, setFilterOpen] = useState(false);
|
|
35
40
|
const [pendingFilters, setPendingFilters] = useState(() => Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
|
|
36
41
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
37
42
|
const [selectedRow, setSelectedRow] = useState(null);
|
|
43
|
+
const [banModalOpen, setBanModalOpen] = useState(false);
|
|
44
|
+
const [banTargetId, setBanTargetId] = useState(null);
|
|
45
|
+
const [banReason, setBanReason] = useState("");
|
|
46
|
+
const banUser = useMutation({
|
|
47
|
+
mutationFn: () => {
|
|
48
|
+
if (!banTargetId)
|
|
49
|
+
throw new Error("No user selected");
|
|
50
|
+
return apiClient.post(ADMIN_ENDPOINTS.USER_HARD_BAN(banTargetId), { reason: banReason.trim() });
|
|
51
|
+
},
|
|
52
|
+
onSuccess: () => {
|
|
53
|
+
toast.showToast("User has been banned.", "success");
|
|
54
|
+
setBanModalOpen(false);
|
|
55
|
+
setBanTargetId(null);
|
|
56
|
+
setBanReason("");
|
|
57
|
+
void queryClient.invalidateQueries({ queryKey: ["admin", "users", "listing"] });
|
|
58
|
+
},
|
|
59
|
+
onError: () => { toast.showToast("Failed to ban user.", "error"); },
|
|
60
|
+
});
|
|
61
|
+
const unbanUser = useMutation({
|
|
62
|
+
mutationFn: (uid) => apiClient.post(ADMIN_ENDPOINTS.USER_UNBAN(uid), {}),
|
|
63
|
+
onSuccess: () => {
|
|
64
|
+
toast.showToast("Ban lifted.", "success");
|
|
65
|
+
void queryClient.invalidateQueries({ queryKey: ["admin", "users", "listing"] });
|
|
66
|
+
},
|
|
67
|
+
onError: () => { toast.showToast("Failed to lift ban.", "error"); },
|
|
68
|
+
});
|
|
38
69
|
const openFilters = useCallback(() => {
|
|
39
70
|
setPendingFilters(Object.fromEntries(FILTER_KEYS.map((k) => [k, table.get(k)])));
|
|
40
71
|
setFilterOpen(true);
|
|
@@ -117,10 +148,26 @@ export function AdminUsersView({ children, ...props }) {
|
|
|
117
148
|
}
|
|
118
149
|
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search users, email, or seller handles", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
119
150
|
{ id: "manage", label: "Manage Selected", variant: "primary", onClick: () => { setSelectedRow(rows.find(r => r.id === selection.selectedIds[0]) ?? null); setDrawerOpen(true); selection.clearSelection(); } },
|
|
120
|
-
] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No users found", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), renderRowActions: (row) =>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
151
|
+
] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No users found", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), renderRowActions: (row) => {
|
|
152
|
+
const ur = row;
|
|
153
|
+
const isBanned = ur.status === "Hard banned";
|
|
154
|
+
return (_jsx(RowActionMenu, { actions: [
|
|
155
|
+
{
|
|
156
|
+
label: "Manage",
|
|
157
|
+
onClick: () => { setSelectedRow(ur); setDrawerOpen(true); },
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
label: ACTIONS.ADMIN["ban-user"].label,
|
|
161
|
+
onClick: () => { setBanTargetId(ur.id); setBanReason(""); setBanModalOpen(true); },
|
|
162
|
+
disabled: isBanned,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
label: ACTIONS.ADMIN["unban-user"].label,
|
|
166
|
+
onClick: () => unbanUser.mutate(ur.id),
|
|
167
|
+
disabled: !isBanned || unbanUser.isPending,
|
|
168
|
+
},
|
|
169
|
+
] }));
|
|
170
|
+
} })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No users found", onRowClick: (row) => { setSelectedRow(row); setDrawerOpen(true); }, selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), _jsx(UsersFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters })] }), _jsx(AdminUserEditorView, { open: drawerOpen, onClose: () => setDrawerOpen(false), userId: selectedRow?.id, displayName: selectedRow?.primary, currentRole: toStringValue(selectedRow?._raw?.role, "user"), currentEmailVerified: Boolean(selectedRow?._raw?.emailVerified), ownedStoreId: toStringValue(selectedRow?._raw?.storeId, "") || undefined, ownedStoreName: toStringValue(selectedRow?._raw?.storeName, "") || undefined, currentIsHardBanned: Boolean((selectedRow?._raw?.isDisabled ?? selectedRow?._raw?.disabled) &&
|
|
124
171
|
selectedRow?._raw?.hardBanReason), currentHardBanReason: toStringValue(selectedRow?._raw?.hardBanReason, "") || undefined, currentSoftBans: Array.isArray(selectedRow?._raw?.softBans)
|
|
125
172
|
? selectedRow._raw.softBans.map((b) => ({
|
|
126
173
|
action: toStringValue(b.action, ""),
|
|
@@ -135,5 +182,5 @@ export function AdminUsersView({ children, ...props }) {
|
|
|
135
182
|
: null,
|
|
136
183
|
bannedBy: toStringValue(b.bannedBy, ""),
|
|
137
184
|
}))
|
|
138
|
-
: undefined })] }));
|
|
185
|
+
: undefined }), _jsxs(Modal, { isOpen: banModalOpen, onClose: () => { setBanModalOpen(false); setBanTargetId(null); setBanReason(""); }, title: ACTIONS.ADMIN["ban-user"].confirmation.title, children: [_jsx(AppText, { size: "sm", color: "muted", className: "mb-4", children: ACTIONS.ADMIN["ban-user"].confirmation.body }), _jsxs(Form, { onSubmit: (e) => { e.preventDefault(); banUser.mutate(); }, children: [_jsx(Input, { label: "Reason", value: banReason, onChange: (e) => setBanReason(e.target.value), placeholder: "e.g. Repeated fraud, scam activity\u2026", required: true }), _jsxs(FormActions, { children: [_jsx(Button, { type: "button", variant: "secondary", onClick: () => { setBanModalOpen(false); setBanTargetId(null); setBanReason(""); }, children: "Cancel" }), _jsx(Button, { type: "submit", variant: "danger", isLoading: banUser.isPending, disabled: !banReason.trim() || banUser.isPending, children: ACTIONS.ADMIN["ban-user"].confirmation.confirmLabel })] })] })] })] }));
|
|
139
186
|
}
|
|
@@ -17,7 +17,7 @@ export interface NotificationDocument {
|
|
|
17
17
|
isRead: boolean;
|
|
18
18
|
readAt?: Date;
|
|
19
19
|
relatedId?: string;
|
|
20
|
-
relatedType?: "order" | "product" | "bid" | "review" | "blog" | "user" | "offer" | "support_ticket";
|
|
20
|
+
relatedType?: "order" | "product" | "bid" | "review" | "blog" | "user" | "offer" | "support_ticket" | "scammer";
|
|
21
21
|
createdAt: Date;
|
|
22
22
|
updatedAt: Date;
|
|
23
23
|
}
|
|
@@ -36,14 +36,7 @@ export function SectionCarousel({ title, description, headingVariant = "editoria
|
|
|
36
36
|
headingClass,
|
|
37
37
|
]
|
|
38
38
|
.filter(Boolean)
|
|
39
|
-
.join(" "), children: title }), headingVariant === "editorial" && (_jsxs("div", { className: `${flex.center} gap-2 mt-1 text-zinc-400 dark:text-zinc-500 text-xs select-none`, "aria-hidden": "true", "data-section": "sectioncarousel-div-355", children: [_jsx(Span, { className: "h-px w-6 bg-current" }), _jsx(Span, { className: "text-xs", children: "\u2736" }), _jsx(Span, { className: "h-px w-6 bg-current" })] })), description && (_jsx(Text, { className: `text-base ${descVariant} mt-2`, children: description }))] }), isLoading ? (_jsx(CarouselSkeleton, { count: skeletonCount })) : (_jsx(HorizontalScroller, { items: items, renderItem: renderItem, perView: perView, gap: gap, autoScroll: autoScroll, autoScrollInterval: autoScrollInterval, keyExtractor: keyExtractor, rows: rows, minItemWidth: minItemWidth,
|
|
40
|
-
/* Default behaviour for homepage sections using cards:
|
|
41
|
-
- show arrows for easier navigation
|
|
42
|
-
- snap to items for a tidy carousel feel
|
|
43
|
-
- show fade edges for visual affordance
|
|
44
|
-
- hide native scrollbar for cleaner appearance
|
|
45
|
-
*/
|
|
46
|
-
showArrows: true, snapToItems: true, showFadeEdges: true, showScrollbar: false, pauseOnHover: true })), viewMoreHref && !isLoading && (_jsx("div", { className: "mt-6 flex justify-center", "data-section": "sectioncarousel-div-356", children: _jsx(TextLink, { href: viewMoreHref, className: `inline-flex items-center gap-1.5 rounded-lg border px-6 py-2.5 text-sm font-medium transition-colors ${useLightText
|
|
39
|
+
.join(" "), children: title }), headingVariant === "editorial" && (_jsxs("div", { className: `${flex.center} gap-2 mt-1 text-zinc-400 dark:text-zinc-500 text-xs select-none`, "aria-hidden": "true", "data-section": "sectioncarousel-div-355", children: [_jsx(Span, { className: "h-px w-6 bg-current" }), _jsx(Span, { className: "text-xs", children: "\u2736" }), _jsx(Span, { className: "h-px w-6 bg-current" })] })), description && (_jsx(Text, { className: `text-base ${descVariant} mt-2`, children: description }))] }), isLoading ? (_jsx(CarouselSkeleton, { count: skeletonCount })) : (_jsx(HorizontalScroller, { items: items, renderItem: renderItem, perView: perView, gap: gap, autoScroll: autoScroll, autoScrollInterval: autoScrollInterval, loop: autoScroll && rows === 1, keyExtractor: keyExtractor, rows: rows, minItemWidth: minItemWidth, showArrows: true, snapToItems: true, showFadeEdges: true, showScrollbar: false, pauseOnHover: true })), viewMoreHref && !isLoading && (_jsx("div", { className: "mt-6 flex justify-center", "data-section": "sectioncarousel-div-356", children: _jsx(TextLink, { href: viewMoreHref, className: `inline-flex items-center gap-1.5 rounded-lg border px-6 py-2.5 text-sm font-medium transition-colors ${useLightText
|
|
47
40
|
? "border-white/40 text-white hover:bg-white/10"
|
|
48
41
|
: "border-zinc-200 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800"}`, children: viewMoreLabel }) }))] })] }));
|
|
49
42
|
}
|
|
@@ -133,8 +133,8 @@ export const homepageSectionsSeedData = [
|
|
|
133
133
|
rows: 2,
|
|
134
134
|
itemsPerRow: 3,
|
|
135
135
|
mobileItemsPerRow: 1,
|
|
136
|
-
autoScroll:
|
|
137
|
-
scrollInterval:
|
|
136
|
+
autoScroll: true,
|
|
137
|
+
scrollInterval: 5000,
|
|
138
138
|
},
|
|
139
139
|
createdAt: daysAgo(90),
|
|
140
140
|
updatedAt: daysAgo(4),
|
|
@@ -152,8 +152,8 @@ export const homepageSectionsSeedData = [
|
|
|
152
152
|
rows: 2,
|
|
153
153
|
itemsPerRow: 3,
|
|
154
154
|
mobileItemsPerRow: 1,
|
|
155
|
-
autoScroll:
|
|
156
|
-
scrollInterval:
|
|
155
|
+
autoScroll: true,
|
|
156
|
+
scrollInterval: 5000,
|
|
157
157
|
},
|
|
158
158
|
createdAt: daysAgo(90),
|
|
159
159
|
updatedAt: daysAgo(3),
|
|
@@ -171,8 +171,8 @@ export const homepageSectionsSeedData = [
|
|
|
171
171
|
rows: 2,
|
|
172
172
|
itemsPerRow: 3,
|
|
173
173
|
mobileItemsPerRow: 1,
|
|
174
|
-
autoScroll:
|
|
175
|
-
scrollInterval:
|
|
174
|
+
autoScroll: true,
|
|
175
|
+
scrollInterval: 5000,
|
|
176
176
|
},
|
|
177
177
|
createdAt: daysAgo(90),
|
|
178
178
|
updatedAt: daysAgo(3),
|
|
@@ -249,8 +249,8 @@ export const homepageSectionsSeedData = [
|
|
|
249
249
|
title: "Top Collectibles Stores",
|
|
250
250
|
subtitle: "Browse our verified seller stores — Pokémon, Hot Wheels, Beyblade X, and more",
|
|
251
251
|
maxStores: 5,
|
|
252
|
-
autoScroll:
|
|
253
|
-
scrollInterval:
|
|
252
|
+
autoScroll: true,
|
|
253
|
+
scrollInterval: 5000,
|
|
254
254
|
},
|
|
255
255
|
createdAt: daysAgo(90),
|
|
256
256
|
updatedAt: daysAgo(5),
|
|
@@ -265,8 +265,8 @@ export const homepageSectionsSeedData = [
|
|
|
265
265
|
title: "Tournaments & Community Events",
|
|
266
266
|
subtitle: "Sales, polls, and collector meetups — stay in the loop",
|
|
267
267
|
maxEvents: 6,
|
|
268
|
-
autoScroll:
|
|
269
|
-
scrollInterval:
|
|
268
|
+
autoScroll: true,
|
|
269
|
+
scrollInterval: 6000,
|
|
270
270
|
},
|
|
271
271
|
createdAt: daysAgo(60),
|
|
272
272
|
updatedAt: daysAgo(4),
|
package/dist/server.d.ts
CHANGED
|
@@ -490,7 +490,7 @@ export { getStoreCapabilities, storeHasCapability, } from "./_internal/server/fe
|
|
|
490
490
|
export { isSoftBanned, getBanSummary } from "./features/auth/server/checkSoftBan";
|
|
491
491
|
export type { UserSoftBan } from "./features/auth/schemas/firestore";
|
|
492
492
|
export type { BannedAction } from "./features/auth/permissions/constants";
|
|
493
|
-
export { buildSitemap, buildRobots, buildManifest, buildDefaultOgImage, DEFAULT_OG_SIZE } from "./_internal/server/features/seo";
|
|
493
|
+
export { buildSitemap, buildRobots, buildManifest, buildDefaultOgImage, DEFAULT_OG_SIZE, resolveOgImageUrl } from "./_internal/server/features/seo";
|
|
494
494
|
export type { SitemapOptions, RobotsOptions, ManifestOptions, DefaultOgOptions } from "./_internal/server/features/seo";
|
|
495
495
|
export { checkActionAllowed } from "./features/admin/utils/checkActionAllowed";
|
|
496
496
|
export { getDisabledRoutes } from "./features/admin/utils/getDisabledRoutes";
|
package/dist/server.js
CHANGED
|
@@ -1366,7 +1366,7 @@ export { getStoreCapabilities, storeHasCapability, } from "./_internal/server/fe
|
|
|
1366
1366
|
// ── Soft ban helpers ──────────────────────────────────────────────────────────
|
|
1367
1367
|
export { isSoftBanned, getBanSummary } from "./features/auth/server/checkSoftBan";
|
|
1368
1368
|
// -- SEO builders (sitemap / robots / manifest / og-image) --
|
|
1369
|
-
export { buildSitemap, buildRobots, buildManifest, buildDefaultOgImage, DEFAULT_OG_SIZE } from "./_internal/server/features/seo";
|
|
1369
|
+
export { buildSitemap, buildRobots, buildManifest, buildDefaultOgImage, DEFAULT_OG_SIZE, resolveOgImageUrl } from "./_internal/server/features/seo";
|
|
1370
1370
|
// -- Action gate + nav route helpers (server-side) --
|
|
1371
1371
|
export { checkActionAllowed } from "./features/admin/utils/checkActionAllowed";
|
|
1372
1372
|
export { getDisabledRoutes } from "./features/admin/utils/getDisabledRoutes";
|