@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.
Files changed (52) hide show
  1. package/dist/_internal/server/features/auctions/og.d.ts +1 -0
  2. package/dist/_internal/server/features/auctions/og.js +2 -1
  3. package/dist/_internal/server/features/blog/og.d.ts +1 -0
  4. package/dist/_internal/server/features/blog/og.js +3 -2
  5. package/dist/_internal/server/features/brands/og.d.ts +1 -0
  6. package/dist/_internal/server/features/brands/og.js +2 -1
  7. package/dist/_internal/server/features/bundles/og.d.ts +1 -0
  8. package/dist/_internal/server/features/bundles/og.js +2 -1
  9. package/dist/_internal/server/features/categories/og.d.ts +1 -0
  10. package/dist/_internal/server/features/categories/og.js +2 -1
  11. package/dist/_internal/server/features/checkout/actions.js +13 -5
  12. package/dist/_internal/server/features/classified/og.d.ts +2 -0
  13. package/dist/_internal/server/features/classified/og.js +2 -1
  14. package/dist/_internal/server/features/digital-code/og.d.ts +2 -0
  15. package/dist/_internal/server/features/digital-code/og.js +2 -1
  16. package/dist/_internal/server/features/events/og.d.ts +1 -0
  17. package/dist/_internal/server/features/events/og.js +3 -1
  18. package/dist/_internal/server/features/live/og.d.ts +2 -0
  19. package/dist/_internal/server/features/live/og.js +2 -1
  20. package/dist/_internal/server/features/pre-orders/og.d.ts +1 -0
  21. package/dist/_internal/server/features/pre-orders/og.js +2 -1
  22. package/dist/_internal/server/features/products/og.d.ts +3 -1
  23. package/dist/_internal/server/features/products/og.js +2 -1
  24. package/dist/_internal/server/features/seo/index.d.ts +1 -1
  25. package/dist/_internal/server/features/seo/index.js +1 -1
  26. package/dist/_internal/server/features/seo/og.d.ts +8 -1
  27. package/dist/_internal/server/features/seo/og.js +28 -8
  28. package/dist/_internal/server/features/stores/og.d.ts +1 -0
  29. package/dist/_internal/server/features/stores/og.js +3 -2
  30. package/dist/_internal/server/features/sublisting-categories/og.d.ts +1 -0
  31. package/dist/_internal/server/features/sublisting-categories/og.js +2 -1
  32. package/dist/_internal/server/jobs/core/onScamReportCreate.js +18 -14
  33. package/dist/_internal/server/jobs/core/onScamReportRejected.js +10 -7
  34. package/dist/_internal/server/jobs/core/onScamReportVerified.js +10 -7
  35. package/dist/client.d.ts +2 -0
  36. package/dist/client.js +1 -0
  37. package/dist/constants/api-endpoints.d.ts +9 -0
  38. package/dist/constants/api-endpoints.js +3 -0
  39. package/dist/features/admin/components/AdminBundlesView.js +20 -2
  40. package/dist/features/admin/components/AdminPayoutsView.js +2 -1
  41. package/dist/features/admin/components/AdminProductsView.js +13 -0
  42. package/dist/features/admin/components/AdminStoresView.js +43 -5
  43. package/dist/features/admin/components/AdminUsersView.js +53 -6
  44. package/dist/features/admin/schemas/firestore.d.ts +1 -1
  45. package/dist/features/blog/components/BlogPostForm.js +1 -0
  46. package/dist/features/categories/components/CategoryForm.js +1 -0
  47. package/dist/features/homepage/components/SectionCarousel.js +1 -8
  48. package/dist/seed/homepage-sections-seed-data.js +10 -10
  49. package/dist/server.d.ts +1 -1
  50. package/dist/server.js +1 -1
  51. package/dist/ui/components/FormField.js +1 -0
  52. 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 coverImage = typeof doc?.coverImage === "string"
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
- const cartSubtotal = orderGroups.reduce((s, { items: g }) => s + g.reduce((gs, { item }) => gs + item.price * item.quantity, 0), 0);
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 groupTotal = group.reduce((sum, { item }) => sum + item.price * item.quantity, 0);
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: item.price,
360
- totalPrice: item.price * item.quantity,
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.price,
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 coverImageUrl = doc?.coverImageUrl ??
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): ReactElement;
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
- export function buildDefaultOgImage({ siteName, tagline, domain }) {
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: 120,
28
- height: 120,
29
- borderRadius: 24,
30
- background: "rgba(255,255,255,0.15)",
31
- marginBottom: 32,
32
- fontSize: 64,
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 { notificationRepository, userRepository } from "../../../../repositories";
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 notificationRepository.create({
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
- body: `Your report for "${name}" has been received. Our team will review it within 48 hours.`,
15
- isRead: false,
16
- entityId: scammerId,
17
- entityType: "scammer",
18
- createdAt: new Date(),
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) => notificationRepository.create({
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
- body: `A report was submitted for "${name}" — ${scamTypeLabel}${platformStr}.${amountStr}`,
40
- isRead: false,
41
- entityId: scammerId,
42
- entityType: "scammer",
43
- createdAt: new Date(),
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 { notificationRepository } from "../../../../repositories";
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 notificationRepository.create({
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
- body: `Your report for "${name}" could not be verified with the evidence provided. You may submit a new report with additional evidence.`,
18
- isRead: false,
19
- entityId: scammerId,
20
- entityType: "scammer",
21
- createdAt: new Date(),
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 { notificationRepository } from "../../../../repositories";
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 notificationRepository.create({
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
- body: `The report for "${name}" has been verified and published to the Scam Registry. Thank you for helping protect the community.`,
18
- isRead: false,
19
- entityId: scammerId,
20
- entityType: "scammer",
21
- createdAt: new Date(),
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: "Mark paid",
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) => (_jsx(RowActionMenu, { actions: [{
97
- label: "Manage",
98
- onClick: () => openEditPanel(row.id),
99
- }] })) })) : (_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)
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) => (_jsx(RowActionMenu, { actions: [{
121
- label: "Manage",
122
- onClick: () => { setSelectedRow(row); setDrawerOpen(true); },
123
- }] })) })) : (_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) &&
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
  }
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
3
  import { Div, Input, Label, RichTextEditor, Select, Stack, TagInput, Text, Textarea, } from "../../../ui";
3
4
  import { Checkbox } from "../../forms/index";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { FormGroup } from "../../../ui";
3
4
  import { Label, Checkbox, RichTextEditor, Text } from "../../../ui";
@@ -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: false,
137
- scrollInterval: 0,
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: false,
156
- scrollInterval: 0,
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: false,
175
- scrollInterval: 0,
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: false,
253
- scrollInterval: 0,
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: false,
269
- scrollInterval: 0,
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";
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
3
  import { Input } from "./Input";
3
4
  import { Select } from "./Select";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "2.7.35",
3
+ "version": "2.7.36",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"