@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.
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +1 -1
- package/dist/_internal/server/jobs/core/auctionSettlement.js +6 -3
- package/dist/_internal/server/jobs/core/bundleStockSync.js +9 -7
- package/dist/_internal/server/jobs/core/onProductStockChange.d.ts +12 -3
- package/dist/_internal/server/jobs/core/onProductStockChange.js +12 -13
- package/dist/_internal/server/jobs/core/onProductWrite.js +1 -3
- package/dist/_internal/server/jobs/handlers/onProductStockChange.js +11 -2
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/field-names.d.ts +4 -3
- package/dist/constants/field-names.js +8 -8
- package/dist/errors/error-codes.d.ts +3 -0
- package/dist/errors/error-codes.js +3 -0
- package/dist/errors/messages.d.ts +3 -0
- package/dist/errors/messages.js +3 -0
- package/dist/features/admin/types/product.types.d.ts +1 -1
- package/dist/features/admin/types/product.types.js +2 -3
- package/dist/features/auctions/actions/bid-actions.d.ts +9 -0
- package/dist/features/auctions/actions/bid-actions.js +61 -0
- package/dist/features/auctions/components/AuctionDetailPageView.d.ts +2 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +5 -4
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +3 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +32 -2
- package/dist/features/auctions/types/index.d.ts +1 -1
- package/dist/features/layout/AppLayoutShell.js +1 -1
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -11
- package/dist/features/products/api/[id]/route.js +4 -8
- package/dist/features/products/api/route.js +1 -3
- package/dist/features/products/components/AuctionsIndexListing.js +3 -11
- package/dist/features/products/components/PrizeDrawsIndexListing.js +2 -10
- package/dist/features/products/components/ProductFilters.d.ts +33 -9
- package/dist/features/products/components/ProductFilters.js +6 -29
- package/dist/features/products/components/ProductForm.js +2 -3
- package/dist/features/products/constants/sieve.d.ts +162 -0
- package/dist/features/products/constants/sieve.js +95 -0
- package/dist/features/products/repository/products.repository.d.ts +5 -0
- package/dist/features/products/repository/products.repository.js +23 -3
- package/dist/features/products/schemas/firestore.d.ts +4 -4
- package/dist/features/products/schemas/firestore.js +2 -3
- package/dist/features/products/schemas/index.d.ts +6 -6
- package/dist/features/products/schemas/index.js +2 -6
- package/dist/features/products/types/index.d.ts +1 -1
- package/dist/features/seller/actions/offer-actions.js +2 -2
- package/dist/features/wishlist/types/index.d.ts +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +6 -4
- package/dist/seed/addresses-seed-data.d.ts +2 -18
- package/dist/seed/addresses-seed-data.js +137 -203
- package/dist/seed/bids-seed-data.d.ts +2 -8
- package/dist/seed/bids-seed-data.js +172 -648
- package/dist/seed/blog-posts-seed-data.d.ts +0 -5
- package/dist/seed/blog-posts-seed-data.js +341 -596
- package/dist/seed/carousel-slides-seed-data.d.ts +0 -5
- package/dist/seed/carousel-slides-seed-data.js +61 -192
- package/dist/seed/carousels-seed-data.js +18 -1
- package/dist/seed/cart-seed-data.d.ts +1 -18
- package/dist/seed/cart-seed-data.js +116 -293
- package/dist/seed/categories-seed-data.d.ts +0 -6
- package/dist/seed/categories-seed-data.js +1409 -1537
- package/dist/seed/claimed-coupons-seed-data.d.ts +0 -11
- package/dist/seed/claimed-coupons-seed-data.js +32 -29
- package/dist/seed/conversations-seed-data.d.ts +0 -6
- package/dist/seed/conversations-seed-data.js +132 -280
- package/dist/seed/coupon-usage-seed-data.d.ts +0 -16
- package/dist/seed/coupon-usage-seed-data.js +30 -44
- package/dist/seed/coupons-seed-data.d.ts +0 -6
- package/dist/seed/coupons-seed-data.js +141 -489
- package/dist/seed/events-seed-data.d.ts +0 -6
- package/dist/seed/events-seed-data.js +201 -379
- package/dist/seed/faq-seed-data.d.ts +0 -14
- package/dist/seed/faq-seed-data.js +609 -1184
- package/dist/seed/grouped-listings-seed-data.d.ts +119 -8
- package/dist/seed/grouped-listings-seed-data.js +148 -233
- package/dist/seed/history-seed-data.d.ts +0 -7
- package/dist/seed/history-seed-data.js +50 -54
- package/dist/seed/homepage-sections-seed-data.d.ts +0 -5
- package/dist/seed/homepage-sections-seed-data.js +83 -175
- package/dist/seed/index.d.ts +1 -1
- package/dist/seed/index.js +1 -1
- package/dist/seed/manifest.js +2 -2
- package/dist/seed/notifications-seed-data.d.ts +0 -6
- package/dist/seed/notifications-seed-data.js +309 -196
- package/dist/seed/offers-seed-data.d.ts +0 -8
- package/dist/seed/offers-seed-data.js +118 -197
- package/dist/seed/orders-seed-data.d.ts +2 -8
- package/dist/seed/orders-seed-data.js +248 -1510
- package/dist/seed/payouts-seed-data.d.ts +0 -6
- package/dist/seed/payouts-seed-data.js +102 -441
- package/dist/seed/products-auctions-seed-data.d.ts +1 -14
- package/dist/seed/products-auctions-seed-data.js +612 -1084
- package/dist/seed/products-preorders-seed-data.d.ts +119 -14
- package/dist/seed/products-preorders-seed-data.js +154 -455
- package/dist/seed/products-standard-seed-data.d.ts +1 -15
- package/dist/seed/products-standard-seed-data.js +1355 -4128
- package/dist/seed/reviews-seed-data.d.ts +2 -8
- package/dist/seed/reviews-seed-data.js +99 -1182
- package/dist/seed/scammers-seed-data.d.ts +0 -15
- package/dist/seed/scammers-seed-data.js +36 -37
- package/dist/seed/sessions-seed-data.d.ts +0 -13
- package/dist/seed/sessions-seed-data.js +58 -335
- package/dist/seed/site-settings-seed-data.d.ts +0 -4
- package/dist/seed/site-settings-seed-data.js +61 -52
- package/dist/seed/store-addresses-seed-data.js +27 -151
- package/dist/seed/store-extensions-seed-data.d.ts +0 -7
- package/dist/seed/store-extensions-seed-data.js +147 -261
- package/dist/seed/stores-seed-data.d.ts +0 -5
- package/dist/seed/stores-seed-data.js +222 -184
- package/dist/seed/sub-listings-seed-data.d.ts +119 -0
- package/dist/seed/sub-listings-seed-data.js +447 -0
- package/dist/seed/support-tickets-seed-data.d.ts +0 -7
- package/dist/seed/support-tickets-seed-data.js +82 -75
- package/dist/seed/users-seed-data.d.ts +0 -5
- package/dist/seed/users-seed-data.js +279 -634
- package/dist/seed/wishlists-seed-data.d.ts +0 -9
- package/dist/seed/wishlists-seed-data.js +36 -52
- package/dist/seo/json-ld.js +6 -4
- package/dist/styles.css +8052 -3
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/search-tokens.d.ts +2 -0
- package/dist/utils/search-tokens.js +21 -0
- package/package.json +2 -2
- 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,
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
30
|
-
if (!
|
|
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(
|
|
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
|
-
|
|
20
|
-
|
|
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(
|
|
24
|
-
if (!
|
|
18
|
+
export function isAvailable(doc) {
|
|
19
|
+
if (!doc)
|
|
25
20
|
return false;
|
|
26
|
-
|
|
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
|
|
41
|
-
if (!isAvailable(
|
|
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
|
|
90
|
-
if (isAvailable(
|
|
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.
|
|
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
|
-
|
|
6
|
-
|
|
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
|
|
80
|
-
readonly
|
|
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
|
-
|
|
85
|
-
|
|
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", "
|
|
123
|
-
published: ["draft", "
|
|
124
|
-
|
|
125
|
-
|
|
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.";
|
package/dist/errors/messages.js
CHANGED
|
@@ -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" | "
|
|
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: "
|
|
11
|
-
{ value: "
|
|
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 && ["
|
|
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" }),
|
|
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" | "
|
|
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"
|
|
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
|
}
|