@mohasinac/appkit 2.7.53 → 2.7.54

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 (123) hide show
  1. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +1 -1
  2. package/dist/_internal/server/jobs/core/auctionSettlement.js +6 -3
  3. package/dist/_internal/server/jobs/core/bundleStockSync.js +9 -7
  4. package/dist/_internal/server/jobs/core/onProductStockChange.d.ts +12 -3
  5. package/dist/_internal/server/jobs/core/onProductStockChange.js +12 -13
  6. package/dist/_internal/server/jobs/core/onProductWrite.js +1 -3
  7. package/dist/_internal/server/jobs/handlers/onProductStockChange.js +11 -2
  8. package/dist/client.d.ts +2 -0
  9. package/dist/client.js +1 -0
  10. package/dist/constants/field-names.d.ts +4 -3
  11. package/dist/constants/field-names.js +8 -8
  12. package/dist/errors/error-codes.d.ts +3 -0
  13. package/dist/errors/error-codes.js +3 -0
  14. package/dist/errors/messages.d.ts +3 -0
  15. package/dist/errors/messages.js +3 -0
  16. package/dist/features/admin/types/product.types.d.ts +1 -1
  17. package/dist/features/admin/types/product.types.js +2 -3
  18. package/dist/features/auctions/actions/bid-actions.d.ts +9 -0
  19. package/dist/features/auctions/actions/bid-actions.js +61 -0
  20. package/dist/features/auctions/components/AuctionDetailPageView.d.ts +2 -1
  21. package/dist/features/auctions/components/AuctionDetailPageView.js +5 -4
  22. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +3 -1
  23. package/dist/features/auctions/components/PlaceBidFormClient.js +32 -2
  24. package/dist/features/auctions/types/index.d.ts +1 -1
  25. package/dist/features/layout/AppLayoutShell.js +1 -1
  26. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -11
  27. package/dist/features/products/api/[id]/route.js +4 -8
  28. package/dist/features/products/api/route.js +1 -3
  29. package/dist/features/products/components/AuctionsIndexListing.js +3 -11
  30. package/dist/features/products/components/PrizeDrawsIndexListing.js +2 -10
  31. package/dist/features/products/components/ProductFilters.d.ts +33 -9
  32. package/dist/features/products/components/ProductFilters.js +6 -29
  33. package/dist/features/products/components/ProductForm.js +2 -3
  34. package/dist/features/products/constants/sieve.d.ts +162 -0
  35. package/dist/features/products/constants/sieve.js +95 -0
  36. package/dist/features/products/repository/products.repository.d.ts +5 -0
  37. package/dist/features/products/repository/products.repository.js +23 -3
  38. package/dist/features/products/schemas/firestore.d.ts +4 -4
  39. package/dist/features/products/schemas/firestore.js +2 -3
  40. package/dist/features/products/schemas/index.d.ts +6 -6
  41. package/dist/features/products/schemas/index.js +2 -6
  42. package/dist/features/products/types/index.d.ts +1 -1
  43. package/dist/features/seller/actions/offer-actions.js +2 -2
  44. package/dist/features/wishlist/types/index.d.ts +1 -1
  45. package/dist/index.d.ts +6 -3
  46. package/dist/index.js +6 -4
  47. package/dist/seed/addresses-seed-data.d.ts +2 -18
  48. package/dist/seed/addresses-seed-data.js +137 -203
  49. package/dist/seed/bids-seed-data.d.ts +2 -8
  50. package/dist/seed/bids-seed-data.js +172 -648
  51. package/dist/seed/blog-posts-seed-data.d.ts +0 -5
  52. package/dist/seed/blog-posts-seed-data.js +341 -596
  53. package/dist/seed/carousel-slides-seed-data.d.ts +0 -5
  54. package/dist/seed/carousel-slides-seed-data.js +61 -192
  55. package/dist/seed/carousels-seed-data.js +18 -1
  56. package/dist/seed/cart-seed-data.d.ts +1 -18
  57. package/dist/seed/cart-seed-data.js +116 -293
  58. package/dist/seed/categories-seed-data.d.ts +0 -6
  59. package/dist/seed/categories-seed-data.js +1409 -1537
  60. package/dist/seed/claimed-coupons-seed-data.d.ts +0 -11
  61. package/dist/seed/claimed-coupons-seed-data.js +32 -29
  62. package/dist/seed/conversations-seed-data.d.ts +0 -6
  63. package/dist/seed/conversations-seed-data.js +132 -280
  64. package/dist/seed/coupon-usage-seed-data.d.ts +0 -16
  65. package/dist/seed/coupon-usage-seed-data.js +30 -44
  66. package/dist/seed/coupons-seed-data.d.ts +0 -6
  67. package/dist/seed/coupons-seed-data.js +141 -489
  68. package/dist/seed/events-seed-data.d.ts +0 -6
  69. package/dist/seed/events-seed-data.js +201 -379
  70. package/dist/seed/faq-seed-data.d.ts +0 -14
  71. package/dist/seed/faq-seed-data.js +609 -1184
  72. package/dist/seed/grouped-listings-seed-data.d.ts +119 -8
  73. package/dist/seed/grouped-listings-seed-data.js +148 -233
  74. package/dist/seed/history-seed-data.d.ts +0 -7
  75. package/dist/seed/history-seed-data.js +50 -54
  76. package/dist/seed/homepage-sections-seed-data.d.ts +0 -5
  77. package/dist/seed/homepage-sections-seed-data.js +83 -175
  78. package/dist/seed/index.d.ts +1 -1
  79. package/dist/seed/index.js +1 -1
  80. package/dist/seed/manifest.js +2 -2
  81. package/dist/seed/notifications-seed-data.d.ts +0 -6
  82. package/dist/seed/notifications-seed-data.js +309 -196
  83. package/dist/seed/offers-seed-data.d.ts +0 -8
  84. package/dist/seed/offers-seed-data.js +118 -197
  85. package/dist/seed/orders-seed-data.d.ts +2 -8
  86. package/dist/seed/orders-seed-data.js +248 -1510
  87. package/dist/seed/payouts-seed-data.d.ts +0 -6
  88. package/dist/seed/payouts-seed-data.js +102 -441
  89. package/dist/seed/products-auctions-seed-data.d.ts +1 -14
  90. package/dist/seed/products-auctions-seed-data.js +612 -1084
  91. package/dist/seed/products-preorders-seed-data.d.ts +119 -14
  92. package/dist/seed/products-preorders-seed-data.js +154 -455
  93. package/dist/seed/products-standard-seed-data.d.ts +1 -15
  94. package/dist/seed/products-standard-seed-data.js +1355 -4128
  95. package/dist/seed/reviews-seed-data.d.ts +2 -8
  96. package/dist/seed/reviews-seed-data.js +99 -1182
  97. package/dist/seed/scammers-seed-data.d.ts +0 -15
  98. package/dist/seed/scammers-seed-data.js +36 -37
  99. package/dist/seed/sessions-seed-data.d.ts +0 -13
  100. package/dist/seed/sessions-seed-data.js +58 -335
  101. package/dist/seed/site-settings-seed-data.d.ts +0 -4
  102. package/dist/seed/site-settings-seed-data.js +61 -52
  103. package/dist/seed/store-addresses-seed-data.js +27 -151
  104. package/dist/seed/store-extensions-seed-data.d.ts +0 -7
  105. package/dist/seed/store-extensions-seed-data.js +147 -261
  106. package/dist/seed/stores-seed-data.d.ts +0 -5
  107. package/dist/seed/stores-seed-data.js +222 -184
  108. package/dist/seed/sub-listings-seed-data.d.ts +119 -0
  109. package/dist/seed/sub-listings-seed-data.js +447 -0
  110. package/dist/seed/support-tickets-seed-data.d.ts +0 -7
  111. package/dist/seed/support-tickets-seed-data.js +82 -75
  112. package/dist/seed/users-seed-data.d.ts +0 -5
  113. package/dist/seed/users-seed-data.js +279 -634
  114. package/dist/seed/wishlists-seed-data.d.ts +0 -9
  115. package/dist/seed/wishlists-seed-data.js +36 -52
  116. package/dist/seo/json-ld.js +6 -4
  117. package/dist/styles.css +8052 -3
  118. package/dist/utils/index.d.ts +1 -0
  119. package/dist/utils/index.js +1 -0
  120. package/dist/utils/search-tokens.d.ts +2 -0
  121. package/dist/utils/search-tokens.js +21 -0
  122. package/package.json +2 -2
  123. package/scripts/seed-cli.mjs +2 -2
@@ -91,5 +91,5 @@ export function DashboardLayoutClient({ variant, groups, permissions, activeHref
91
91
  const adminGroups = variant === "admin"
92
92
  ? filteredGroups
93
93
  : groups;
94
- return (_jsxs(_Fragment, { children: [variant === "admin" && (_jsx(AdminSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activePath: activeHref, groups: adminGroups, onCloseMobile: close, onToggle: toggle, className: className })), variant === "store" && (_jsx(StoreSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activeHref: activeHref, items: [], groups: filteredGroups, onCloseMobile: close, onToggle: toggle, className: className })), variant === "user" && (_jsx(UserSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, items: filteredGroups.flatMap((g) => g.items), groups: filteredGroups, onCloseMobile: close, onToggle: toggle, className: className })), _jsx("div", { className: "w-full px-5 py-8 md:pl-14 md:pr-6 lg:pl-16 lg:pr-10 min-h-[calc(100dvh-var(--header-height,3.5rem))]", children: _jsx("div", { className: "w-full max-w-screen-2xl mx-auto", children: children }) })] }));
94
+ return (_jsxs(_Fragment, { children: [variant === "admin" && (_jsx(AdminSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activePath: activeHref, groups: adminGroups, onCloseMobile: close, onToggle: toggle, className: className })), variant === "store" && (_jsx(StoreSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activeHref: activeHref, items: [], groups: filteredGroups, onCloseMobile: close, onToggle: toggle, className: className })), variant === "user" && (_jsx(UserSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, items: filteredGroups.flatMap((g) => g.items), groups: filteredGroups, onCloseMobile: close, onToggle: toggle, className: className })), _jsx("div", { className: "w-full flex-1 flex flex-col px-5 py-8 md:pl-14 md:pr-6 lg:pl-16 lg:pr-10 min-h-[calc(100dvh-var(--header-height,3.5rem))]", children: _jsx("div", { className: "w-full flex-1 max-w-screen-2xl mx-auto", children: children }) })] }));
95
95
  }
@@ -1,12 +1,11 @@
1
1
  import { bidRepository, orderRepository, productRepository, } from "../../../../repositories";
2
2
  import { sendNotification } from "../../../../features/admin/actions/notification-actions";
3
- import { ProductStatusValues } from "../../../../features/products/schemas/firestore";
4
3
  import { AUCTION_MESSAGES } from "../handlers/messages";
5
4
  async function settleAuction(ctx, product) {
6
5
  const activeBids = await bidRepository.getActiveByProduct(product.id);
7
6
  const batch = ctx.db.batch();
8
7
  if (activeBids.length === 0) {
9
- productRepository.updateStatusInBatch(batch, product.id, ProductStatusValues.OUT_OF_STOCK);
8
+ productRepository.updateStatusInBatch(batch, product.id, "archived");
10
9
  await batch.commit();
11
10
  ctx.logger.info(AUCTION_MESSAGES.NO_BIDS_LOG(product.id));
12
11
  return;
@@ -25,7 +24,11 @@ async function settleAuction(ctx, product) {
25
24
  currency: winnerEntry.data.currency,
26
25
  auctionProductId: product.id,
27
26
  });
28
- productRepository.updateStatusInBatch(batch, product.id, ProductStatusValues.SOLD);
27
+ // Two-axis model: mark as sold without changing publish status
28
+ batch.update(ctx.db.collection("products").doc(product.id), {
29
+ isSold: true,
30
+ availableQuantity: 0,
31
+ });
29
32
  await batch.commit();
30
33
  // Fan-out notifications after batch commits.
31
34
  await sendNotification({
@@ -9,11 +9,13 @@
9
9
  import { CATEGORY_FIELDS, PRODUCT_FIELDS, COMMON_FIELDS } from "../../../../constants/field-names";
10
10
  const CATEGORIES_COLLECTION = "categories";
11
11
  const PRODUCT_COLLECTION = "products";
12
- const UNAVAILABLE = new Set([
13
- PRODUCT_FIELDS.STATUS_VALUES.SOLD,
14
- PRODUCT_FIELDS.STATUS_VALUES.OUT_OF_STOCK,
15
- PRODUCT_FIELDS.STATUS_VALUES.DISCONTINUED,
16
- ]);
12
+ function isMemberAvailable(data) {
13
+ if (data.isSold)
14
+ return false;
15
+ if (typeof data.availableQuantity === "number" && data.availableQuantity <= 0)
16
+ return false;
17
+ return data.status === PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED;
18
+ }
17
19
  async function computeBundleStockStatus(productIds, ctx) {
18
20
  if (productIds.length === 0)
19
21
  return "out_of_stock";
@@ -26,8 +28,8 @@ async function computeBundleStockStatus(productIds, ctx) {
26
28
  if (snap.size < chunk.length)
27
29
  return "out_of_stock";
28
30
  for (const doc of snap.docs) {
29
- const status = doc.data().status;
30
- if (!status || UNAVAILABLE.has(status))
31
+ const data = doc.data();
32
+ if (!isMemberAvailable(data))
31
33
  return "out_of_stock";
32
34
  }
33
35
  }
@@ -11,13 +11,22 @@
11
11
  * status didn't transition between "available" and "unavailable".
12
12
  */
13
13
  import type { JobContext } from "../runtime/types";
14
- export declare function isAvailable(status?: string | null): boolean;
14
+ export declare function isAvailable(doc: {
15
+ isSold?: boolean;
16
+ availableQuantity?: number;
17
+ status?: string;
18
+ } | null): boolean;
15
19
  export declare function syncBundlesForProduct(productId: string, ctx: JobContext): Promise<number>;
16
20
  export declare function syncGroupedListingsForProduct(productId: string, ctx: JobContext): Promise<number>;
21
+ export interface ProductStockSnapshot {
22
+ isSold?: boolean;
23
+ availableQuantity?: number;
24
+ status?: string;
25
+ }
17
26
  export interface HandleProductStockChangeInput {
18
27
  productId: string;
19
- beforeStatus: string | null;
20
- afterStatus: string | null;
28
+ before: ProductStockSnapshot | null;
29
+ after: ProductStockSnapshot | null;
21
30
  isDelete: boolean;
22
31
  }
23
32
  export declare function handleProductStockChange(input: HandleProductStockChangeInput, ctx: JobContext): Promise<void>;
@@ -14,16 +14,15 @@ import { CATEGORY_FIELDS, PRODUCT_FIELDS, COMMON_FIELDS } from "../../../../cons
14
14
  const CATEGORIES_COLLECTION = "categories";
15
15
  const GROUPED_LISTINGS_COLLECTION = "groupedListings";
16
16
  const PRODUCT_COLLECTION = "products";
17
- const UNAVAILABLE_STATUSES = new Set([
18
- PRODUCT_FIELDS.STATUS_VALUES.SOLD,
19
- PRODUCT_FIELDS.STATUS_VALUES.OUT_OF_STOCK,
20
- PRODUCT_FIELDS.STATUS_VALUES.DISCONTINUED,
21
- ]);
22
17
  const MIN_ACTIVE_DEFAULT = 2;
23
- export function isAvailable(status) {
24
- if (!status)
18
+ export function isAvailable(doc) {
19
+ if (!doc)
25
20
  return false;
26
- return !UNAVAILABLE_STATUSES.has(status);
21
+ if (doc.isSold)
22
+ return false;
23
+ if (typeof doc.availableQuantity === "number" && doc.availableQuantity <= 0)
24
+ return false;
25
+ return doc.status === PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED;
27
26
  }
28
27
  async function recomputeBundleStatus(productIds, ctx) {
29
28
  if (productIds.length === 0)
@@ -37,8 +36,8 @@ async function recomputeBundleStatus(productIds, ctx) {
37
36
  if (snap.size < chunk.length)
38
37
  return "out_of_stock";
39
38
  for (const doc of snap.docs) {
40
- const status = doc.data().status;
41
- if (!isAvailable(status))
39
+ const data = doc.data();
40
+ if (!isAvailable(data))
42
41
  return "out_of_stock";
43
42
  }
44
43
  }
@@ -86,8 +85,8 @@ export async function syncGroupedListingsForProduct(productId, ctx) {
86
85
  .where("__name__", "in", chunk)
87
86
  .get();
88
87
  for (const doc of productSnap.docs) {
89
- const status = doc.data().status;
90
- if (isAvailable(status))
88
+ const data = doc.data();
89
+ if (isAvailable(data))
91
90
  activeCount++;
92
91
  }
93
92
  }
@@ -105,7 +104,7 @@ export async function syncGroupedListingsForProduct(productId, ctx) {
105
104
  return updated;
106
105
  }
107
106
  export async function handleProductStockChange(input, ctx) {
108
- const { productId, beforeStatus, afterStatus, isDelete } = input;
107
+ const { productId, before: beforeStatus, after: afterStatus, isDelete } = input;
109
108
  const beforeAvailable = isAvailable(beforeStatus);
110
109
  const afterAvailable = isAvailable(afterStatus);
111
110
  const stateFlipped = beforeAvailable !== afterAvailable;
@@ -6,9 +6,7 @@ import { categoriesRepository, storeRepository, } from "../../../../repositories
6
6
  import { ProductStatusValues } from "../../../../features/products/schemas/firestore";
7
7
  import { PRODUCT_FIELDS } from "../../../../constants/field-names";
8
8
  const UNAVAILABLE_PRODUCT_STATUSES = new Set([
9
- ProductStatusValues.SOLD,
10
- ProductStatusValues.OUT_OF_STOCK,
11
- ProductStatusValues.DISCONTINUED,
9
+ ProductStatusValues.ARCHIVED,
12
10
  ]);
13
11
  void UNAVAILABLE_PRODUCT_STATUSES;
14
12
  async function getParentIds(categoryId) {
@@ -1,9 +1,18 @@
1
1
  import { handleProductStockChange } from "../core/onProductStockChange";
2
+ function toSnapshot(doc) {
3
+ if (!doc)
4
+ return null;
5
+ return {
6
+ isSold: doc.isSold,
7
+ availableQuantity: doc.availableQuantity,
8
+ status: doc.status,
9
+ };
10
+ }
2
11
  export const onProductStockChangeHandler = async (event, ctx) => {
3
12
  await handleProductStockChange({
4
13
  productId: event.params.productId,
5
- beforeStatus: event.before?.status ?? null,
6
- afterStatus: event.after?.status ?? null,
14
+ before: toSnapshot(event.before),
15
+ after: toSnapshot(event.after),
7
16
  isDelete: !event.after,
8
17
  }, ctx);
9
18
  };
package/dist/client.d.ts CHANGED
@@ -155,6 +155,8 @@ export type { MarketplaceBundleCardProps, MarketplaceBundleCardData, Marketplace
155
155
  export { CompareOverlay } from "./features/products/components/CompareOverlay";
156
156
  export type { CompareOverlayProps, CompareOverlayLabels, CompareProductLike, } from "./features/products/components/CompareOverlay";
157
157
  export { COMPARE_MAX_ITEMS } from "./features/products/constants/action-defs";
158
+ export { STANDARD_SORT_OPTIONS, STANDARD_PUBLIC_SORT_OPTIONS, AUCTION_SORT_OPTIONS, AUCTION_PUBLIC_SORT_OPTIONS, PREORDER_SORT_OPTIONS, BUNDLE_SORT_OPTIONS, PRIZE_DRAW_SORT_OPTIONS, SORT_OPTIONS_BY_LISTING_TYPE, } from "./features/products/constants/sieve";
159
+ export type { SortOption } from "./features/products/constants/sieve";
158
160
  export { MessagesView } from "./features/account/components/MessagesView";
159
161
  export type { MessagesViewProps, MessagesViewLabels } from "./features/account/components/MessagesView";
160
162
  export { ChatList } from "./features/account/components/ChatList";
package/dist/client.js CHANGED
@@ -181,6 +181,7 @@ export { InteractiveProductCard } from "./features/products/index";
181
181
  export { MarketplaceBundleCard } from "./features/products/components/MarketplaceBundleCard";
182
182
  export { CompareOverlay } from "./features/products/components/CompareOverlay";
183
183
  export { COMPARE_MAX_ITEMS } from "./features/products/constants/action-defs";
184
+ export { STANDARD_SORT_OPTIONS, STANDARD_PUBLIC_SORT_OPTIONS, AUCTION_SORT_OPTIONS, AUCTION_PUBLIC_SORT_OPTIONS, PREORDER_SORT_OPTIONS, BUNDLE_SORT_OPTIONS, PRIZE_DRAW_SORT_OPTIONS, SORT_OPTIONS_BY_LISTING_TYPE, } from "./features/products/constants/sieve";
184
185
  // Messages — RTDB-pinged Firestore conversations (D5 + VC7)
185
186
  export { MessagesView } from "./features/account/components/MessagesView";
186
187
  export { ChatList } from "./features/account/components/ChatList";
@@ -22,6 +22,7 @@ export declare const PRODUCT_FIELDS: {
22
22
  readonly CURRENCY: "currency";
23
23
  readonly STOCK_QUANTITY: "stockQuantity";
24
24
  readonly AVAILABLE_QUANTITY: "availableQuantity";
25
+ readonly IS_SOLD: "isSold";
25
26
  readonly MAIN_IMAGE: "mainImage";
26
27
  readonly IMAGES: "images";
27
28
  readonly VIDEO: "video";
@@ -71,14 +72,14 @@ export declare const PRODUCT_FIELDS: {
71
72
  readonly PRIZE_REVEAL_WINDOW_END: "prizeRevealWindowEnd";
72
73
  readonly PRIZE_REVEAL_DEADLINE: "prizeRevealDeadline";
73
74
  readonly IS_PART_OF_BUNDLE: "isPartOfBundle";
75
+ readonly SEARCH_TOKENS: "searchTokens";
74
76
  readonly CREATED_AT: "createdAt";
75
77
  readonly UPDATED_AT: "updatedAt";
76
78
  readonly STATUS_VALUES: {
77
79
  readonly DRAFT: "draft";
78
80
  readonly PUBLISHED: "published";
79
- readonly OUT_OF_STOCK: "out_of_stock";
80
- readonly DISCONTINUED: "discontinued";
81
- readonly SOLD: "sold";
81
+ readonly IN_REVIEW: "in_review";
82
+ readonly ARCHIVED: "archived";
82
83
  };
83
84
  readonly CONDITION_VALUES: {
84
85
  readonly NEW: "new";
@@ -25,6 +25,7 @@ export const PRODUCT_FIELDS = {
25
25
  CURRENCY: "currency",
26
26
  STOCK_QUANTITY: "stockQuantity",
27
27
  AVAILABLE_QUANTITY: "availableQuantity",
28
+ IS_SOLD: "isSold",
28
29
  MAIN_IMAGE: "mainImage",
29
30
  IMAGES: "images",
30
31
  VIDEO: "video",
@@ -76,14 +77,14 @@ export const PRODUCT_FIELDS = {
76
77
  PRIZE_REVEAL_WINDOW_END: "prizeRevealWindowEnd",
77
78
  PRIZE_REVEAL_DEADLINE: "prizeRevealDeadline",
78
79
  IS_PART_OF_BUNDLE: "isPartOfBundle",
80
+ SEARCH_TOKENS: "searchTokens",
79
81
  CREATED_AT: "createdAt",
80
82
  UPDATED_AT: "updatedAt",
81
83
  STATUS_VALUES: {
82
84
  DRAFT: "draft",
83
85
  PUBLISHED: "published",
84
- OUT_OF_STOCK: "out_of_stock",
85
- DISCONTINUED: "discontinued",
86
- SOLD: "sold",
86
+ IN_REVIEW: "in_review",
87
+ ARCHIVED: "archived",
87
88
  },
88
89
  CONDITION_VALUES: {
89
90
  NEW: "new",
@@ -119,11 +120,10 @@ export const PRODUCT_FIELDS = {
119
120
  },
120
121
  };
121
122
  export const PRODUCT_STATUS_TRANSITIONS = {
122
- draft: ["published", "discontinued"],
123
- published: ["draft", "out_of_stock", "discontinued"],
124
- out_of_stock: ["published", "draft", "discontinued"],
125
- sold: ["discontinued"],
126
- discontinued: ["draft"],
123
+ draft: ["published", "in_review"],
124
+ published: ["draft", "archived"],
125
+ in_review: ["published", "draft", "archived"],
126
+ archived: ["draft"],
127
127
  };
128
128
  // ============================================================================
129
129
  // ORDER FIELDS
@@ -6,6 +6,9 @@ export declare const BID_ERROR_CODES: {
6
6
  readonly INCREMENT_VIOLATED: "BID_INCREMENT_TOO_LOW";
7
7
  readonly SELF_BID: "BID_SELF_BID";
8
8
  readonly USER_BANNED: "BID_USER_BANNED";
9
+ readonly BUY_NOW_UNAVAILABLE: "BID_BUY_NOW_UNAVAILABLE";
10
+ readonly BUY_NOW_BIDS_STARTED: "BID_BUY_NOW_BIDS_STARTED";
11
+ readonly BUY_NOW_NO_PRICE: "BID_BUY_NOW_NO_PRICE";
9
12
  };
10
13
  export declare const OFFER_ERROR_CODES: {
11
14
  readonly DISABLED: "OFFER_DISABLED";
@@ -6,6 +6,9 @@ export const BID_ERROR_CODES = {
6
6
  INCREMENT_VIOLATED: "BID_INCREMENT_TOO_LOW",
7
7
  SELF_BID: "BID_SELF_BID",
8
8
  USER_BANNED: "BID_USER_BANNED",
9
+ BUY_NOW_UNAVAILABLE: "BID_BUY_NOW_UNAVAILABLE",
10
+ BUY_NOW_BIDS_STARTED: "BID_BUY_NOW_BIDS_STARTED",
11
+ BUY_NOW_NO_PRICE: "BID_BUY_NOW_NO_PRICE",
9
12
  };
10
13
  export const OFFER_ERROR_CODES = {
11
14
  DISABLED: "OFFER_DISABLED",
@@ -347,6 +347,9 @@ export declare const ERROR_MESSAGES: {
347
347
  readonly BID_TOO_LOW: "Your bid must be higher than the current bid";
348
348
  readonly INCREMENT_TOO_LOW: "Your bid does not meet the minimum increment requirement";
349
349
  readonly OWN_AUCTION: "You cannot bid on your own auction";
350
+ readonly BUY_NOW_UNAVAILABLE: "Buy Now is not available for this auction";
351
+ readonly BUY_NOW_BIDS_STARTED: "Buy Now is no longer available — bids have already been placed";
352
+ readonly BUY_NOW_NO_PRICE: "This auction does not have a Buy Now price";
350
353
  };
351
354
  readonly CONTACT: {
352
355
  readonly SEND_FAILED: "Failed to send your message. Please try again.";
@@ -374,6 +374,9 @@ export const ERROR_MESSAGES = {
374
374
  BID_TOO_LOW: "Your bid must be higher than the current bid",
375
375
  INCREMENT_TOO_LOW: "Your bid does not meet the minimum increment requirement",
376
376
  OWN_AUCTION: "You cannot bid on your own auction",
377
+ BUY_NOW_UNAVAILABLE: "Buy Now is not available for this auction",
378
+ BUY_NOW_BIDS_STARTED: "Buy Now is no longer available — bids have already been placed",
379
+ BUY_NOW_NO_PRICE: "This auction does not have a Buy Now price",
377
380
  },
378
381
  CONTACT: {
379
382
  SEND_FAILED: "Failed to send your message. Please try again.",
@@ -4,7 +4,7 @@
4
4
  * These types extend/complement the core ProductItem with admin-only fields
5
5
  * for managing auctions, pre-orders, stock, pricing, and lifecycle.
6
6
  */
7
- export type AdminProductStatus = "draft" | "published" | "out_of_stock" | "discontinued" | "sold";
7
+ export type AdminProductStatus = "draft" | "published" | "in_review" | "archived";
8
8
  export type AdminProductDrawerMode = "create" | "edit" | "delete" | null;
9
9
  /**
10
10
  * Admin product form/management type.
@@ -7,7 +7,6 @@
7
7
  export const ADMIN_PRODUCT_STATUS_OPTIONS = [
8
8
  { value: "draft", label: "Draft" },
9
9
  { value: "published", label: "Published" },
10
- { value: "out_of_stock", label: "Out of Stock" },
11
- { value: "discontinued", label: "Discontinued" },
12
- { value: "sold", label: "Sold" },
10
+ { value: "in_review", label: "In Review" },
11
+ { value: "archived", label: "Archived" },
13
12
  ];
@@ -15,6 +15,15 @@ export interface PlaceBidResult {
15
15
  bid: BidDocument;
16
16
  }
17
17
  export declare function placeBid(userId: string, userEmail: string, input: PlaceBidInput): Promise<PlaceBidResult>;
18
+ export interface BuyNowAuctionInput {
19
+ productId: string;
20
+ }
21
+ export interface BuyNowAuctionResult {
22
+ orderId: string;
23
+ amount: number;
24
+ currency: string;
25
+ }
26
+ export declare function buyNowAuction(userId: string, userName: string, userEmail: string, input: BuyNowAuctionInput): Promise<BuyNowAuctionResult>;
18
27
  export declare function listBidsByProduct(productId: string, params?: {
19
28
  page?: number;
20
29
  pageSize?: number;
@@ -26,6 +26,9 @@ export async function placeBid(userId, userEmail, input) {
26
26
  throw new NotFoundError(ERROR_MESSAGES.BID.AUCTION_NOT_FOUND);
27
27
  if (!isAuctionListing(product))
28
28
  throw new ValidationError(ERROR_MESSAGES.BID.NOT_AN_AUCTION);
29
+ if (product.isSold) {
30
+ throw new ValidationError(ERROR_MESSAGES.BID.AUCTION_ENDED, { code: BID_ERROR_CODES.AUCTION_ENDED });
31
+ }
29
32
  if (product.auctionEndDate) {
30
33
  const endDate = resolveDate(product.auctionEndDate);
31
34
  if (endDate && endDate.getTime() < Date.now()) {
@@ -152,6 +155,64 @@ export async function placeBid(userId, userEmail, input) {
152
155
  });
153
156
  return { bid };
154
157
  }
158
+ export async function buyNowAuction(userId, userName, userEmail, input) {
159
+ const { productId } = input;
160
+ const product = await productRepository.findById(productId);
161
+ if (!product)
162
+ throw new NotFoundError(ERROR_MESSAGES.BID.AUCTION_NOT_FOUND);
163
+ if (!isAuctionListing(product))
164
+ throw new ValidationError(ERROR_MESSAGES.BID.NOT_AN_AUCTION);
165
+ if (product.isSold) {
166
+ throw new ValidationError(ERROR_MESSAGES.BID.AUCTION_ENDED, { code: BID_ERROR_CODES.AUCTION_ENDED });
167
+ }
168
+ if (product.auctionEndDate) {
169
+ const endDate = resolveDate(product.auctionEndDate);
170
+ if (endDate && endDate.getTime() < Date.now()) {
171
+ throw new ValidationError(ERROR_MESSAGES.BID.AUCTION_ENDED, { code: BID_ERROR_CODES.AUCTION_ENDED });
172
+ }
173
+ }
174
+ if (product.storeId) {
175
+ const store = await storeRepository.findById(product.storeId);
176
+ if (store?.ownerId === userId) {
177
+ throw new AuthorizationError(ERROR_MESSAGES.BID.OWN_AUCTION);
178
+ }
179
+ }
180
+ const buyNowPrice = typeof product.buyNowPrice === "number" ? product.buyNowPrice : null;
181
+ if (buyNowPrice === null || buyNowPrice <= 0) {
182
+ throw new ValidationError(ERROR_MESSAGES.BID.BUY_NOW_NO_PRICE, { code: BID_ERROR_CODES.BUY_NOW_NO_PRICE });
183
+ }
184
+ if (product.bidsHaveStarted === true) {
185
+ throw new ValidationError(ERROR_MESSAGES.BID.BUY_NOW_BIDS_STARTED, { code: BID_ERROR_CODES.BUY_NOW_BIDS_STARTED });
186
+ }
187
+ const currency = getDefaultCurrency();
188
+ let orderId = "";
189
+ await unitOfWork.runBatch((batch) => {
190
+ const orderRef = unitOfWork.orders.createFromAuction(batch, {
191
+ productId,
192
+ productTitle: product.title,
193
+ userId,
194
+ userName,
195
+ userEmail,
196
+ storeId: product.storeId,
197
+ amount: buyNowPrice,
198
+ currency,
199
+ auctionProductId: productId,
200
+ });
201
+ orderId = orderRef.id;
202
+ unitOfWork.products.updateInBatch(batch, productId, {
203
+ isSold: true,
204
+ availableQuantity: 0,
205
+ auctionEndDate: new Date(),
206
+ });
207
+ });
208
+ serverLogger.info("buyNowAuction", {
209
+ orderId,
210
+ productId,
211
+ userId,
212
+ amount: buyNowPrice,
213
+ });
214
+ return { orderId, amount: buyNowPrice, currency };
215
+ }
155
216
  export async function listBidsByProduct(productId, params) {
156
217
  const result = await bidRepository.list({
157
218
  filters: `productId==${productId}`,
@@ -10,7 +10,8 @@ export interface AuctionDetailPageViewProps {
10
10
  */
11
11
  initialAuction?: import("../../products/schemas/firestore").ProductDocument | null;
12
12
  onPlaceBid?: (input: PlaceBidInput) => Promise<unknown>;
13
+ onBuyNow?: () => Promise<unknown>;
13
14
  /** SSR-loaded productFeatures (platform + store-scope). See ProductDetailPageView for semantics. */
14
15
  productFeatures?: ProductFeatureDocument[];
15
16
  }
16
- export declare function AuctionDetailPageView({ id, initialAuction, onPlaceBid, productFeatures }: AuctionDetailPageViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
17
+ export declare function AuctionDetailPageView({ id, initialAuction, onPlaceBid, onBuyNow, productFeatures }: AuctionDetailPageViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
@@ -40,7 +40,7 @@ function renderAuctionStoreReviews(storeReviews) {
40
40
  const avg = storeReviews.reduce((s, r) => s + r.rating, 0) / storeReviews.length;
41
41
  return (_jsxs(Section, { className: "mt-10", children: [_jsx(Heading, { level: 2, className: "mb-2 text-xl font-semibold text-zinc-900 dark:text-zinc-50", children: "Store Reviews" }), _jsxs(Div, { className: "mb-4 flex items-center gap-3", children: [_jsx(Span, { className: "text-3xl font-bold text-zinc-900 dark:text-zinc-50", children: avg.toFixed(1) }), _jsxs(Div, { children: [_jsx(Row, { gap: "xs", children: [1, 2, 3, 4, 5].map((star) => _jsx(Span, { className: star <= Math.round(avg) ? "text-amber-400" : "text-zinc-200 dark:text-zinc-700", children: "\u2605" }, star)) }), _jsxs(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: [storeReviews.length, " review", storeReviews.length !== 1 ? "s" : ""] })] })] }), _jsx(Stack, { gap: "sm", children: storeReviews.slice(0, 10).map((review) => (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-white dark:bg-zinc-900/60 p-4 space-y-1.5", children: [_jsx(Row, { justify: "between", align: "center", children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Span, { className: "text-sm font-medium text-zinc-800 dark:text-zinc-200", children: review.userName }), _jsx(Row, { gap: "xs", children: [1, 2, 3, 4, 5].map((star) => _jsx(Span, { className: `text-xs ${star <= review.rating ? "text-amber-400" : "text-zinc-200 dark:text-zinc-700"}`, children: "\u2605" }, star)) })] }) }), review.title && _jsx(Text, { className: "text-sm font-semibold text-zinc-800 dark:text-zinc-100", children: review.title }), _jsx(Text, { className: "text-sm text-zinc-600 dark:text-zinc-400 leading-relaxed", children: review.comment }), _jsx(Text, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: review.productTitle })] }, review.id))) })] }));
42
42
  }
43
- export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, productFeatures }) {
43
+ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, onBuyNow, productFeatures }) {
44
44
  const product = initialAuction !== undefined
45
45
  ? (initialAuction ?? undefined)
46
46
  : await productRepository.findByIdOrSlug(id).catch(() => undefined);
@@ -79,6 +79,7 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, pr
79
79
  const isEnded = endDate ? endDate < new Date() : false;
80
80
  const bidCount = typeof p.bidCount === "number" ? p.bidCount : 0;
81
81
  const buyNowPrice = typeof p.buyNowPrice === "number" ? p.buyNowPrice : null;
82
+ const bidsHaveStarted = p.bidsHaveStarted === true;
82
83
  const condition = typeof p.condition === "string" ? p.condition : null;
83
84
  const featured = p.featured === true;
84
85
  const shippingPaidBy = p.shippingPaidBy;
@@ -116,9 +117,9 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, pr
116
117
  price: typeof p.price === "number" ? p.price : undefined,
117
118
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
118
119
  storeName: typeof p.storeName === "string" ? p.storeName : undefined,
119
- } }), _jsxs(Container, { size: "xl", className: "px-4 py-6", children: [_jsxs("div", { className: "mb-4 flex items-center justify-between flex-wrap gap-2", children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-1.5 text-xs text-zinc-500 dark:text-zinc-400 flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.AUCTIONS), className: CLS_BREADCRUMB_LINK, children: "Auctions" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "text-zinc-700 dark:text-zinc-300 truncate max-w-[200px]", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(AuctionDetailView, { renderGallery: () => (_jsx(ProductGalleryClient, { images: images, productName: title })), renderInfo: () => renderAuctionInfoPanel({ title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref }), renderBidForm: () => onPlaceBid ? (_jsx(Div, { id: "auction-bid-form", className: "space-y-3", children: _jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-3", children: [_jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(Span, { className: "text-xl font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsxs(Span, { className: "text-xs text-zinc-500", children: [bidCount, " ", bidCount === 1 ? "bid" : "bids", " \u00B7 min increment ", formatCurrency(minBidIncrement, currency)] })] }), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, minBidIncrement: minBidIncrement, currency: currency, isEnded: isEnded, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, triggerClassName: "w-full" })] }) })) : (
120
+ } }), _jsxs(Container, { size: "xl", className: "px-4 py-6", children: [_jsxs("div", { className: "mb-4 flex items-center justify-between flex-wrap gap-2", children: [_jsxs("nav", { "aria-label": "Breadcrumb", className: "flex items-center gap-1.5 text-xs text-zinc-500 dark:text-zinc-400 flex-wrap", children: [_jsx(Link, { href: String(ROUTES.HOME), className: CLS_BREADCRUMB_LINK, children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.AUCTIONS), className: CLS_BREADCRUMB_LINK, children: "Auctions" }), category && (_jsxs(_Fragment, { children: [_jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: CLS_BREADCRUMB_LINK, children: categoryName || category })] })), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "text-zinc-700 dark:text-zinc-300 truncate max-w-[200px]", children: title })] }), _jsx(ShareButton, { title: title })] }), _jsx(AuctionDetailView, { renderGallery: () => (_jsx(ProductGalleryClient, { images: images, productName: title })), renderInfo: () => renderAuctionInfoPanel({ title, currentBid, currency, bidCount, isEnded, endDate, buyNowPrice, featured, freeShipping, condition, category, categoryName, brand, brandSlug, productFeatures, features, descriptionHtml, safeSeller, storeHref }), renderBidForm: () => onPlaceBid ? (_jsx(Div, { id: "auction-bid-form", className: "space-y-3", children: _jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-3", children: [_jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(Span, { className: "text-xl font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsxs(Span, { className: "text-xs text-zinc-500", children: [bidCount, " ", bidCount === 1 ? "bid" : "bids", " \u00B7 min increment ", formatCurrency(minBidIncrement, currency)] })] }), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, minBidIncrement: minBidIncrement, currency: currency, isEnded: isEnded, buyNowPrice: buyNowPrice, bidsHaveStarted: bidsHaveStarted, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] }) })) : (
120
121
  /* Read-only bid panel — shown when no bid action is wired (preview/demo) */
121
- _jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [_jsx(Div, { className: "space-y-1", children: _jsxs(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: ["Starting bid: ", formatCurrency(startingBid, currency), " \u00B7 min increment ", formatCurrency(minBidIncrement, currency)] }) }), _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number", placeholder: `At least ${formatCurrency(currentBid + minBidIncrement, currency)}`, min: currentBid + minBidIncrement, "aria-label": "Your bid amount", disabled: isEnded }), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowPrice !== null && !isEnded && (_jsxs(Button, { variant: "secondary", size: "md", className: "w-full", children: ["Buy Now \u2014 ", formatCurrency(buyNowPrice, currency)] }))] }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) }))] })), renderMobileBidForm: () => !isEnded && onPlaceBid ? (_jsxs(Div, { className: "lg:hidden rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-4 space-y-3", children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Span, { className: "text-base font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsxs(Span, { className: "text-xs text-zinc-500", children: [bidCount, " bids"] })] }), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, minBidIncrement: minBidIncrement, currency: currency, isEnded: isEnded, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, triggerClassName: "w-full" })] })) : !isEnded ? (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-4 lg:hidden", children: [_jsxs(Row, { align: "center", gap: "sm", className: "mb-3", children: [_jsx(Span, { className: "text-base font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsxs(Span, { className: "text-xs text-zinc-500", children: [bidCount, " bids"] })] }), _jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Place Bid" })] })) : null, renderSublistingSection: sublistingCategoryId
122
+ _jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [_jsx(Div, { className: "space-y-1", children: _jsxs(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: ["Starting bid: ", formatCurrency(startingBid, currency), " \u00B7 min increment ", formatCurrency(minBidIncrement, currency)] }) }), _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number", placeholder: `At least ${formatCurrency(currentBid + minBidIncrement, currency)}`, min: currentBid + minBidIncrement, "aria-label": "Your bid amount", disabled: isEnded }), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded, children: isEnded ? "Auction Ended" : "Place Bid" }), buyNowPrice !== null && !isEnded && !bidsHaveStarted && (_jsxs(Button, { variant: "secondary", size: "md", className: "w-full", disabled: true, children: ["Buy Now \u2014 ", formatCurrency(buyNowPrice, currency)] }))] }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) }))] })), renderMobileBidForm: () => !isEnded && onPlaceBid ? (_jsxs(Div, { className: "lg:hidden rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-4 space-y-3", children: [_jsxs(Row, { align: "center", gap: "sm", children: [_jsx(Span, { className: "text-base font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsxs(Span, { className: "text-xs text-zinc-500", children: [bidCount, " bids"] })] }), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, minBidIncrement: minBidIncrement, currency: currency, isEnded: isEnded, buyNowPrice: buyNowPrice, bidsHaveStarted: bidsHaveStarted, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] })) : !isEnded ? (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-4 lg:hidden", children: [_jsxs(Row, { align: "center", gap: "sm", className: "mb-3", children: [_jsx(Span, { className: "text-base font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsxs(Span, { className: "text-xs text-zinc-500", children: [bidCount, " bids"] })] }), _jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Place Bid" })] })) : null, renderSublistingSection: sublistingCategoryId
122
123
  ? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
123
124
  : undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-zinc-700 dark:text-zinc-300" })) : undefined, specsContent: specs.length > 0 ? (_jsx("dl", { className: "divide-y divide-zinc-100 dark:divide-zinc-800 rounded-xl border border-zinc-100 dark:border-zinc-800 overflow-hidden text-sm", children: specs.map((s, i) => (_jsxs("div", { className: "flex gap-4 px-4 py-3 bg-white dark:bg-zinc-900 even:bg-zinc-50 dark:even:bg-zinc-800/50", children: [_jsx("dt", { className: "w-36 flex-shrink-0 font-medium text-zinc-700 dark:text-zinc-300", children: s.name }), _jsxs("dd", { className: "flex-1 text-zinc-600 dark:text-zinc-400", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, customTabs: customSections.map((s) => ({
124
125
  id: s.id,
@@ -140,7 +141,7 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, pr
140
141
  if (r.id === product.id || r.listingType !== "auction")
141
142
  return false;
142
143
  const s = r.status;
143
- if (s && ["sold", "out_of_stock", "archived", "discontinued", "draft"].includes(s))
144
+ if (s && ["archived", "in_review", "draft"].includes(s))
144
145
  return false;
145
146
  if (r.isSold === true)
146
147
  return false;
@@ -11,11 +11,13 @@ export interface PlaceBidFormClientProps {
11
11
  currency: string;
12
12
  isEnded: boolean;
13
13
  buyNowPrice: number | null;
14
+ bidsHaveStarted?: boolean;
14
15
  bidCount: number;
15
16
  tags?: string[];
16
17
  onPlaceBid: (input: PlaceBidInput) => Promise<unknown>;
18
+ onBuyNow?: () => Promise<unknown>;
17
19
  }
18
- export declare function PlaceBidFormClient({ productId, currentBid, startingBid, minBidIncrement, currency, isEnded, buyNowPrice, bidCount, tags, onPlaceBid, }: PlaceBidFormClientProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function PlaceBidFormClient({ productId, currentBid, startingBid, minBidIncrement, currency, isEnded, buyNowPrice, bidsHaveStarted, bidCount, tags, onPlaceBid, onBuyNow, }: PlaceBidFormClientProps): import("react/jsx-runtime").JSX.Element;
19
21
  /**
20
22
  * `PlaceBidModalButton` — opens a modal that hosts the same PlaceBidFormClient.
21
23
  *
@@ -11,13 +11,43 @@ const BID_ERROR_DISPLAY = {
11
11
  BID_SELF_BID: "You cannot bid on your own listing.",
12
12
  BID_USER_BANNED: "Your account is currently restricted from bidding. Contact support.",
13
13
  };
14
- export function PlaceBidFormClient({ productId, currentBid, startingBid, minBidIncrement, currency, isEnded, buyNowPrice, bidCount, tags = [], onPlaceBid, }) {
14
+ export function PlaceBidFormClient({ productId, currentBid, startingBid, minBidIncrement, currency, isEnded, buyNowPrice, bidsHaveStarted = false, bidCount, tags = [], onPlaceBid, onBuyNow, }) {
15
15
  const minBid = currentBid + minBidIncrement;
16
16
  const [bidAmount, setBidAmount] = useState(String(minBid));
17
17
  const [isPending, startTransition] = useTransition();
18
+ const [isBuyNowPending, startBuyNowTransition] = useTransition();
18
19
  const [error, setError] = useState(null);
19
20
  const [success, setSuccess] = useState(false);
20
21
  const [showLoginModal, setShowLoginModal] = useState(false);
22
+ const buyNowAvailable = buyNowPrice !== null && !isEnded && !bidsHaveStarted && !!onBuyNow;
23
+ function handleBuyNow() {
24
+ if (!onBuyNow)
25
+ return;
26
+ setError(null);
27
+ setSuccess(false);
28
+ startBuyNowTransition(async () => {
29
+ try {
30
+ const result = await onBuyNow();
31
+ if (result &&
32
+ typeof result === "object" &&
33
+ "ok" in result &&
34
+ result.ok === false) {
35
+ const r = result;
36
+ setError(r.error ?? "Buy Now failed. Please try again.");
37
+ return;
38
+ }
39
+ setSuccess(true);
40
+ }
41
+ catch (err) {
42
+ if (isAuthError(err)) {
43
+ setShowLoginModal(true);
44
+ }
45
+ else {
46
+ setError(err instanceof Error ? err.message : "Buy Now failed. Please try again.");
47
+ }
48
+ }
49
+ });
50
+ }
21
51
  function handleSubmit(e) {
22
52
  e.preventDefault();
23
53
  const amount = Number(bidAmount);
@@ -52,7 +82,7 @@ export function PlaceBidFormClient({ productId, currentBid, startingBid, minBidI
52
82
  }
53
83
  });
54
84
  }
55
- return (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [_jsxs(Div, { className: "space-y-1", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-xs text-zinc-500", children: "Current bid" }), _jsx(Text, { className: "text-xs text-zinc-500", children: "Starting bid" })] }), _jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(Span, { className: "text-xl font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsx(Span, { className: "text-sm text-zinc-500", children: formatCurrency(startingBid, currency) })] }), _jsxs(Text, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: [bidCount, " ", bidCount === 1 ? "bid" : "bids", " \u00B7 min increment", " ", formatCurrency(minBidIncrement, currency)] })] }), _jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to place a bid. Please log in or create an account to continue." }), _jsx("form", { onSubmit: handleSubmit, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number", value: bidAmount, onChange: (e) => setBidAmount(e.target.value), placeholder: `At least ${formatCurrency(minBid, currency)}`, min: minBid, step: minBidIncrement, "aria-label": "Your bid amount", disabled: isEnded || isPending }), error && (_jsx(Text, { className: "text-xs text-red-600 dark:text-red-400", children: error })), success && (_jsx(Text, { className: "text-xs text-emerald-600 dark:text-emerald-400", children: "\u2713 Bid placed successfully!" })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded || isPending, type: "submit", children: isPending ? "Placing Bid…" : isEnded ? "Auction Ended" : "Place Bid" }), buyNowPrice !== null && !isEnded && (_jsxs(Button, { variant: "secondary", size: "md", className: "w-full", type: "button", children: ["Buy Now \u2014 ", formatCurrency(buyNowPrice, currency)] }))] }) }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) }))] }));
85
+ return (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [_jsxs(Div, { className: "space-y-1", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-xs text-zinc-500", children: "Current bid" }), _jsx(Text, { className: "text-xs text-zinc-500", children: "Starting bid" })] }), _jsxs(Row, { justify: "between", align: "baseline", children: [_jsx(Span, { className: "text-xl font-bold text-primary-600 dark:text-primary-400", children: formatCurrency(currentBid, currency) }), _jsx(Span, { className: "text-sm text-zinc-500", children: formatCurrency(startingBid, currency) })] }), _jsxs(Text, { className: "text-xs text-zinc-400 dark:text-zinc-500", children: [bidCount, " ", bidCount === 1 ? "bid" : "bids", " \u00B7 min increment", " ", formatCurrency(minBidIncrement, currency)] })] }), _jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to place a bid. Please log in or create an account to continue." }), _jsx("form", { onSubmit: handleSubmit, children: _jsxs(Stack, { gap: "sm", children: [_jsx(Input, { type: "number", value: bidAmount, onChange: (e) => setBidAmount(e.target.value), placeholder: `At least ${formatCurrency(minBid, currency)}`, min: minBid, step: minBidIncrement, "aria-label": "Your bid amount", disabled: isEnded || isPending }), error && (_jsx(Text, { className: "text-xs text-red-600 dark:text-red-400", children: error })), success && (_jsx(Text, { className: "text-xs text-emerald-600 dark:text-emerald-400", children: "\u2713 Bid placed successfully!" })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isEnded || isPending, type: "submit", children: isPending ? "Placing Bid…" : isEnded ? "Auction Ended" : "Place Bid" }), buyNowAvailable && (_jsx(Button, { variant: "secondary", size: "md", className: "w-full", type: "button", disabled: isBuyNowPending || isPending, onClick: handleBuyNow, children: isBuyNowPending ? "Processing…" : `Buy Now ${formatCurrency(buyNowPrice, currency)}` }))] }) }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) }))] }));
56
86
  }
57
87
  /**
58
88
  * `PlaceBidModalButton` — opens a modal that hosts the same PlaceBidFormClient.
@@ -14,7 +14,7 @@ export interface AuctionItem {
14
14
  currentBid?: number;
15
15
  bidCount: number;
16
16
  auctionEndDate: string;
17
- status: "published" | "draft" | "archived" | "sold";
17
+ status: "published" | "draft" | "in_review" | "archived";
18
18
  sellerId: string;
19
19
  storeId?: string;
20
20
  storeSlug?: string;
@@ -147,5 +147,5 @@ export function AppLayoutShell({ children, navItems, sidebarItems = [], sidebarS
147
147
  opacity: darkBackground.overlay?.opacity ?? 0,
148
148
  },
149
149
  };
150
- return (_jsx(QueryClientProvider, { client: queryClient, children: _jsxs(Div, { className: "flex min-h-screen w-full flex-col overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { className: "relative flex w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 ${hasBottomActions ? "mb-28" : "mb-16"} md:mb-0`, children: _jsx(Div, { className: contentClassName ?? "w-full px-5 py-6 md:px-6 lg:px-8", children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
150
+ return (_jsx(QueryClientProvider, { client: queryClient, children: _jsxs(Div, { className: "flex min-h-screen w-full flex-col overflow-x-clip transition-colors duration-300", children: [_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: normalizedLightBackground, darkMode: normalizedDarkBackground }), _jsxs(Div, { ref: headerRef, className: "sticky top-0 z-50 w-full", children: [_jsx(TitleBar, { onToggleSidebar: handleTogglePublicSidebar, sidebarOpen: sidebarOpen, onSearchToggle: () => setSearchOpen((prev) => !prev), searchOpen: searchOpen, brandName: brandName, brandShortName: brandShortName, siteLogoUrl: siteLogoUrl, logoHref: logoHref, promotionsHref: promotionsHref, cartHref: cartHref, wishlistHref: wishlistHref, userId: userId, profileHref: profileHref, loginHref: loginHref, registerHref: registerHref, user: user, navSlot: titleBarNavSlot, notificationSlot: titleBarNotificationSlot, devSlot: titleBarDevSlot, promoStripText: titleBarPromoStripText, isDark: theme === "dark", onToggleTheme: showThemeToggle ? toggleTheme : undefined, onBeforeToggleDashboardNav: handleBeforeDashboardNavToggle, suppressDashboardNav: suppressDashboardNav, hideSidebarToggle: hideSidebarToggle }), _jsx(NavbarWithSettings, { navItems: navItems, hiddenNavItems: hiddenNavItems, permissions: authUser?.permissions }), searchOpen && (searchSlotRenderer ? searchSlotRenderer(() => setSearchOpen(false)) : searchSlot)] }), eventBannerSlot, _jsx(AutoBreadcrumbs, {}), _jsxs(Div, { className: "relative flex w-full flex-1 overflow-x-clip", children: [_jsx(SidebarLayout, { isOpen: sidebarOpen, ariaLabel: "Secondary navigation", header: user ? (_jsx(SidebarUserHeader, { user: user, onClose: () => setSidebarOpen(false) })) : (_jsx(SidebarGuestHeader, { sidebarTitle: sidebarTitle, onClose: () => setSidebarOpen(false) })), onClose: () => setSidebarOpen(false), children: sidebarContent }), _jsx(Main, { id: "main-content", className: `w-full flex-1 flex flex-col ${hasBottomActions ? "mb-28" : "mb-16"} md:mb-0`, children: _jsx(Div, { className: `flex-1 ${contentClassName ?? "w-full px-5 py-6 md:px-6 lg:px-8"}`, children: children }) })] }), _jsx(BackToTop, {}), _jsx(FooterLayout, { ...footer }), _jsx(BottomActions, {}), _jsx(BottomNavbar, { user: user, homeHref: homeHref, shopHref: shopHref, cartHref: cartHref, profileHref: profileHref, loginHref: loginHref, onSearchToggle: () => setSearchOpen((prev) => !prev), navItems: navItems, onMoreToggle: hasDashboardNav ? toggleDashboardNav : handleTogglePublicSidebar }), _jsx(UnsavedChangesModal, {})] }) }));
151
151
  }