@mohasinac/appkit 4.12.0 → 4.12.2

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 (68) hide show
  1. package/dist/_internal/server/features/auctions/service.d.ts +7 -1
  2. package/dist/_internal/server/features/auctions/service.js +13 -5
  3. package/dist/_internal/server/features/checkout/actions.d.ts +0 -15
  4. package/dist/_internal/server/features/checkout/actions.js +7 -3
  5. package/dist/_internal/server/features/orders/actions.js +5 -3
  6. package/dist/_internal/server/functions/firestore.d.ts +1 -2
  7. package/dist/_internal/server/functions/firestore.js +1 -9
  8. package/dist/_internal/server/jobs/core/index.d.ts +0 -1
  9. package/dist/_internal/server/jobs/core/index.js +0 -1
  10. package/dist/_internal/server/jobs/core/offerExpiry.js +33 -1
  11. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -2
  12. package/dist/_internal/server/jobs/handlers/index.js +1 -2
  13. package/dist/_internal/shared/features/auctions/config.d.ts +34 -0
  14. package/dist/_internal/shared/features/auctions/config.js +47 -3
  15. package/dist/_internal/shared/listing-types/auction/config.js +1 -1
  16. package/dist/_internal/shared/listing-types/digital-code/config.js +1 -1
  17. package/dist/_internal/shared/listing-types/live/config.js +1 -1
  18. package/dist/_internal/shared/listing-types/pre-order/config.js +1 -1
  19. package/dist/features/account/components/NotificationBell.js +1 -1
  20. package/dist/features/admin/components/AdminNotificationsView.js +4 -1
  21. package/dist/features/admin/components/AdminOrderEditorView.d.ts +7 -1
  22. package/dist/features/admin/components/AdminOrderEditorView.js +16 -4
  23. package/dist/features/admin/components/AdminOrdersView.js +2 -1
  24. package/dist/features/auctions/actions/bid-actions.js +71 -9
  25. package/dist/features/auctions/components/AuctionDetailPageView.js +8 -2
  26. package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -3
  27. package/dist/features/auctions/components/PlaceBidFormClient.js +89 -14
  28. package/dist/features/auctions/schemas/bid-input.d.ts +19 -6
  29. package/dist/features/auctions/schemas/bid-input.js +19 -7
  30. package/dist/features/auth/repository/user.repository.js +6 -0
  31. package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
  32. package/dist/features/filters/FilterFacetSection.js +1 -1
  33. package/dist/features/homepage/components/WhatsAppCommunitySection.js +1 -1
  34. package/dist/features/layout/BackToTop.js +1 -1
  35. package/dist/features/layout/BottomActions.js +1 -1
  36. package/dist/features/layout/FooterLayout.js +2 -2
  37. package/dist/features/layout/TitleBarLayout.js +1 -1
  38. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -3
  39. package/dist/features/products/components/MarketplaceBundleCard.js +2 -2
  40. package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -2
  41. package/dist/features/products/components/PrizeDrawCollage.js +1 -1
  42. package/dist/features/products/components/PrizeDrawItemsEditor.js +1 -1
  43. package/dist/features/products/components/ProductDetailPageView.js +1 -1
  44. package/dist/features/products/components/ProductGrid.js +3 -3
  45. package/dist/features/products/repository/products.repository.d.ts +0 -4
  46. package/dist/features/products/repository/products.repository.js +0 -15
  47. package/dist/features/promotions/repository/coupons.repository.d.ts +12 -0
  48. package/dist/features/promotions/repository/coupons.repository.js +12 -0
  49. package/dist/features/seller/components/SellerSidebar.js +1 -1
  50. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +279 -0
  51. package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +1 -1
  52. package/dist/jobs.d.ts +1 -1
  53. package/dist/jobs.js +1 -1
  54. package/dist/seed/homepage-sections-seed-data.js +5 -1
  55. package/dist/seed/site-settings-seed-data.js +18 -0
  56. package/dist/styles.css +55 -1
  57. package/dist/tailwind-utilities.css +1 -1
  58. package/dist/tokens/color-pairs.js +1 -0
  59. package/dist/tokens/tokens.css +33 -0
  60. package/dist/ui/components/Button.style.css +21 -0
  61. package/dist/ui/components/ImageLightbox.js +7 -4
  62. package/dist/ui/components/surface-tokens.d.ts +7 -0
  63. package/dist/ui/components/surface-tokens.js +7 -0
  64. package/package.json +1 -1
  65. package/dist/_internal/server/jobs/core/onBidPlaced.d.ts +0 -15
  66. package/dist/_internal/server/jobs/core/onBidPlaced.js +0 -63
  67. package/dist/_internal/server/jobs/handlers/onBidPlaced.d.ts +0 -3
  68. package/dist/_internal/server/jobs/handlers/onBidPlaced.js +0 -9
@@ -51,6 +51,7 @@ export const SURFACE_TEXT_PAIR_MAP = {
51
51
  textMutedVar: "var(--appkit-color-info)",
52
52
  textFaintVar: "var(--appkit-color-text-muted)",
53
53
  },
54
+ frost: ON_PRIMARY_PAIR,
54
55
  "overlay-xs": ON_PRIMARY_PAIR,
55
56
  "overlay-sm": ON_PRIMARY_PAIR,
56
57
  "overlay-md": ON_PRIMARY_PAIR,
@@ -284,6 +284,31 @@
284
284
  --appkit-color-info-surface: #f0f9ff; /* sky-50 */
285
285
  --appkit-color-star: #facc15; /* yellow-400 — star ratings (decorative convention) */
286
286
 
287
+ /* Solid status fills — the OVERLAY counterpart to the `-surface` tints above.
288
+ `-surface` is a light tint in light themes and a dark tint in dark themes,
289
+ so it must pair with the matching `--appkit-color-STATUS` ink (which flips
290
+ the opposite way) to stay readable. That pairing is correct for an inline
291
+ chip, but too pale for a badge sitting on a product photo. These `-solid`
292
+ values are deliberately NOT theme-inverted: they stay a dark, saturated
293
+ fill in every theme so `-on-solid` can stay white in every theme.
294
+ Pairing rule, using `warning` as the worked example:
295
+ chip -> bg-warning-surface + text-warning
296
+ overlay -> bg-warning-solid + text-warning-on-solid
297
+ NB: never write a curly brace in a comment in this file. audit-theme-drift
298
+ matches each rule block with a non-greedy `:root ... ` regex, so a stray
299
+ closing brace inside a comment truncates the block and silently hides
300
+ every token declared below it from the drift check.
301
+ Both naive spellings are broken in exactly one theme, which is why
302
+ `audit-status-color-pairs.mjs` blocks them. */
303
+ --appkit-color-success-solid: #15803d; /* green-700 */
304
+ --appkit-color-success-on-solid: #ffffff;
305
+ --appkit-color-warning-solid: #b45309; /* amber-700 */
306
+ --appkit-color-warning-on-solid: #ffffff;
307
+ --appkit-color-error-solid: #b91c1c; /* red-700 */
308
+ --appkit-color-error-on-solid: #ffffff;
309
+ --appkit-color-info-solid: #0369a1; /* sky-700 */
310
+ --appkit-color-info-on-solid: #ffffff;
311
+
287
312
  --appkit-color-focus-ring: #0d9488; /* primary-600 = teal */
288
313
  }
289
314
 
@@ -616,6 +641,14 @@
616
641
  etc. — the highest of those is 65). Set above tooltip so it's the
617
642
  unconditional top of the interactive-overlay stack. */
618
643
  --appkit-z-confirm: 90;
644
+ /* Back-to-top floating control — the frontmost layer. It previously shared
645
+ `--appkit-z-bottom-nav` (40), so it tied with the bottom navbar and lost
646
+ to every sticky toolbar, drawer and toast on the page: the button was
647
+ scrolled to but unclickable (or entirely hidden) on most long pages,
648
+ which is exactly the surface it exists for. It is a small, dismissible,
649
+ non-blocking affordance, so sitting above the overlay stack costs
650
+ nothing — it never covers content, only floats in the corner gutter. */
651
+ --appkit-z-back-to-top: 95;
619
652
 
620
653
  /* Layout dimensions */
621
654
  --appkit-navbar-height: 2.5rem; /* h-10 = 40px (mobile), h-12 = 48px (md) */
@@ -205,9 +205,30 @@
205
205
  animation: appkit-spin 1s linear infinite;
206
206
  }
207
207
 
208
+ /*
209
+ * The content wrapper exists purely so the label paints above the ripple
210
+ * (z-index above). It must NOT introduce a new sizing context — before it
211
+ * existed, a child was a direct flex item of the button and could resolve
212
+ * `width:100%` / `height:100%` against the button's own definite box.
213
+ *
214
+ * Without `flex: 1 1 auto` + `align-self: stretch` this span is shrink-to-fit
215
+ * in BOTH axes, so any fill-style child (`<MediaImage>`, an `absolute inset-0`
216
+ * overlay, a `<Image fill>`) collapses to 0x0 and the button renders blank.
217
+ * That is exactly what happened to every image-tile button in the app when
218
+ * the ripple landed.
219
+ *
220
+ * `justify-content` / `gap` are inherited so the button's own `justify` and
221
+ * `gap` props keep reaching the real children now that they sit one level
222
+ * deeper than the flex container that declares them.
223
+ */
208
224
  .appkit-button__content {
209
225
  display: inline-flex;
210
226
  align-items: center;
227
+ justify-content: inherit;
228
+ gap: inherit;
229
+ flex: 1 1 auto;
230
+ align-self: stretch;
231
+ min-width: 0;
211
232
  }
212
233
 
213
234
  .appkit-button__content--loading {
@@ -8,7 +8,7 @@ import { Div } from "./Div";
8
8
  import { Text, Span } from "./Typography";
9
9
  import { MediaImage } from "../../features/media/MediaImage";
10
10
  import { getYouTubeVideoId } from "../../utils/media-url";
11
- const CLS_CLOSE_BTN = "w-10 h-10 p-0 !min-h-0 rounded-full bg-white/15 hover:bg-error-surface text-white flex items-center justify-center";
11
+ const CLS_CLOSE_BTN = "w-10 h-10 p-0 !min-h-0 rounded-full bg-white/15 hover:bg-error-solid text-error-on-solid flex items-center justify-center";
12
12
  const MIN_ZOOM = 10;
13
13
  const MAX_ZOOM = 300;
14
14
  const ZOOM_STEP = 20;
@@ -124,19 +124,22 @@ export function ImageLightbox({ images, activeIndex, onClose, onNavigate, showTh
124
124
  const hasMultiple = images.length > 1;
125
125
  const youtubeId = image.kind === "video" ? getYouTubeVideoId(image.src) : null;
126
126
  const iconBtnClass = "w-10 h-10 p-0 !min-h-0 rounded-full bg-white/15 hover:bg-white/30 text-white flex items-center justify-center";
127
- return createPortal(_jsxs("div", { ref: overlayRef, tabIndex: -1, className: "appkit-lightbox", role: "dialog", "aria-modal": "true", "aria-label": "Image lightbox", onKeyDown: handleKeyDown, onWheel: handleWheel, children: [_jsxs("div", { className: "absolute top-0 left-0 right-0 z-10 flex items-center justify-between px-4 py-3 bg-gradient-to-b from-black/60 to-transparent", children: [_jsx(Span, { size: "sm", weight: "medium", className: "text-white/70", children: hasMultiple ? `${currentIndex + 1} / ${images.length}` : "" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => adjustZoom(-ZOOM_STEP), className: iconBtnClass, "aria-label": "Zoom out", disabled: zoom <= MIN_ZOOM, children: _jsx(ZoomOut, { className: "w-4 h-4" }) }), _jsxs("button", { type: "button", onClick: () => { setZoom(100); setRotation(0); }, className: "text-white/70 hover:text-white text-xs font-mono min-w-[3rem] text-center", "aria-label": "Reset zoom", children: [zoom, "%"] }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => adjustZoom(ZOOM_STEP), className: iconBtnClass, "aria-label": "Zoom in", disabled: zoom >= MAX_ZOOM, children: _jsx(ZoomIn, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => setRotation((r) => (r + 90) % 360), className: iconBtnClass, "aria-label": "Rotate 90\u00B0", children: _jsx(RotateCw, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: toggleFullscreen, className: iconBtnClass, "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen", children: isFullscreen ? _jsx(Minimize2, { className: "w-4 h-4" }) : _jsx(Maximize2, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: onClose, className: CLS_CLOSE_BTN, "aria-label": "Close lightbox", children: _jsx(X, { className: "w-5 h-5" }) })] })] }), hasMultiple && (_jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => navigate(-1), className: "absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 p-0 !min-h-0 rounded-full bg-white/15 hover:bg-white/30 text-white z-10 flex items-center justify-center", "aria-label": "Previous image", children: _jsx(ChevronLeft, { className: "w-7 h-7" }) })), _jsxs("div", { className: "appkit-lightbox__image-wrap", style: { cursor: zoom > 100 ? "grab" : "default" }, children: [image.kind === "video" ? (youtubeId ? (_jsx("iframe", { src: `https://www.youtube-nocookie.com/embed/${youtubeId}?playsinline=1&rel=0`, title: image.alt ?? "Video", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowFullScreen: true, className: "appkit-lightbox__img appkit-lightbox__video border-0" })) : (_jsx("video", { src: image.src, poster: image.poster, controls: true, playsInline: true, "aria-label": image.alt ?? "Video", className: "appkit-lightbox__img appkit-lightbox__video", style: {
127
+ return createPortal(_jsxs("div", { ref: overlayRef, tabIndex: -1, className: "appkit-lightbox", role: "dialog", "aria-modal": "true", "aria-label": "Image lightbox", onKeyDown: handleKeyDown, onWheel: handleWheel, children: [_jsxs("div", { className: "absolute top-0 left-0 right-0 z-10 flex items-center justify-between px-4 py-3 bg-gradient-to-b from-black/60 to-transparent", children: [_jsx(Span, { size: "sm", weight: "medium", className: "text-white/70", children: hasMultiple ? `${currentIndex + 1} / ${images.length}` : "" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => adjustZoom(-ZOOM_STEP), className: iconBtnClass, "aria-label": "Zoom out", disabled: zoom <= MIN_ZOOM, children: _jsx(ZoomOut, { className: "w-4 h-4" }) }), _jsxs(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => { setZoom(100); setRotation(0); }, className: "text-white/70 hover:text-white text-xs font-mono min-w-[3rem] text-center p-0 !min-h-0", "aria-label": "Reset zoom", children: [zoom, "%"] }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => adjustZoom(ZOOM_STEP), className: iconBtnClass, "aria-label": "Zoom in", disabled: zoom >= MAX_ZOOM, children: _jsx(ZoomIn, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => setRotation((r) => (r + 90) % 360), className: iconBtnClass, "aria-label": "Rotate 90\u00B0", children: _jsx(RotateCw, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: toggleFullscreen, className: iconBtnClass, "aria-label": isFullscreen ? "Exit fullscreen" : "Enter fullscreen", children: isFullscreen ? _jsx(Minimize2, { className: "w-4 h-4" }) : _jsx(Maximize2, { className: "w-4 h-4" }) }), _jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: onClose, className: CLS_CLOSE_BTN, "aria-label": "Close lightbox", children: _jsx(X, { className: "w-5 h-5" }) })] })] }), hasMultiple && (_jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => navigate(-1), className: "absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 p-0 !min-h-0 rounded-full bg-white/15 hover:bg-white/30 text-white z-10 flex items-center justify-center", "aria-label": "Previous image", children: _jsx(ChevronLeft, { className: "w-7 h-7" }) })), _jsxs("div", { className: "appkit-lightbox__image-wrap", style: { cursor: zoom > 100 ? "grab" : "default" }, children: [image.kind === "video" ? (youtubeId ? (_jsx("iframe", { src: `https://www.youtube-nocookie.com/embed/${youtubeId}?playsinline=1&rel=0`, title: image.alt ?? "Video", allow: "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share", allowFullScreen: true, className: "appkit-lightbox__img appkit-lightbox__video border-0" })) : (_jsx("video", { src: image.src, poster: image.poster, controls: true, playsInline: true, "aria-label": image.alt ?? "Video", className: "appkit-lightbox__img appkit-lightbox__video", style: {
128
128
  transform: `scale(${zoom / 100}) rotate(${rotation}deg)`,
129
129
  transition: "transform 0.2s ease",
130
130
  } }))) : (_jsx(MediaImage, { src: image.src, alt: image.alt ?? "", size: "hero", objectFit: "contain", className: "appkit-lightbox__img", style: {
131
131
  transform: `scale(${zoom / 100}) rotate(${rotation}deg)`,
132
132
  transition: "transform 0.2s ease",
133
- } })), image.badge && (_jsx(Div, { className: "absolute left-3 top-3 rounded bg-black/70 px-2 py-1 text-xs font-semibold text-white pointer-events-none", children: image.badge }))] }), (image.caption || image.sub) && (_jsxs("div", { className: "flex-shrink-0 text-center px-8 pb-2", children: [image.caption && (_jsx(Text, { size: "sm", variant: "secondary", className: "!text-white/80", children: image.caption })), image.sub && (_jsx(Text, { size: "sm", variant: "secondary", className: "!text-white/50", children: image.sub }))] })), showThumbnails && images.length > 1 && (_jsx("div", { className: "flex-shrink-0 flex gap-2 overflow-x-auto px-4 pb-3 justify-center", children: images.map((thumb, i) => (_jsx("button", { type: "button", onClick: () => {
133
+ } })), image.badge && (_jsx(Div, { className: "absolute left-3 top-3 rounded bg-black/70 px-2 py-1 text-xs font-semibold text-white pointer-events-none", children: image.badge }))] }), (image.caption || image.sub) && (_jsxs("div", { className: "flex-shrink-0 text-center px-8 pb-2", children: [image.caption && (_jsx(Text, { size: "sm", variant: "secondary", className: "!text-white/80", children: image.caption })), image.sub && (_jsx(Text, { size: "sm", variant: "secondary", className: "!text-white/50", children: image.sub }))] })), showThumbnails && images.length > 1 && (_jsx("div", { className: "flex-shrink-0 flex gap-2 overflow-x-auto px-4 pb-3 justify-center", children: images.map((thumb, i) => (_jsx(Button, { variant: "ghost", size: "sm", type: "button", onClick: () => {
134
134
  setCurrentIndex(i);
135
135
  setZoom(100);
136
136
  setRotation(0);
137
137
  onNavigate?.(i);
138
138
  }, className: [
139
- "h-14 w-14 flex-shrink-0 overflow-hidden rounded-lg border-2 transition-all",
139
+ // p-0/!min-h-0 neutralise the Button's own padding and min-height
140
+ // this is a 56px image tile, and without them the primitive's
141
+ // internal spacing stretches the thumbnail strip (Root Cause #15).
142
+ "h-14 w-14 flex-shrink-0 overflow-hidden rounded-lg border-2 transition-all p-0 !min-h-0",
140
143
  i === currentIndex
141
144
  ? "scale-110 border-white"
142
145
  : "border-transparent opacity-50 hover:opacity-90",
@@ -14,6 +14,13 @@ export declare const SURFACE_MAP: {
14
14
  readonly "danger-surface": "bg-[var(--appkit-color-error-surface)]";
15
15
  readonly "warning-surface": "bg-[var(--appkit-color-warning-surface)]";
16
16
  readonly "info-surface": "bg-[var(--appkit-color-info-surface)]";
17
+ /** Translucent WHITE chip on a dark/branded backdrop (hero banner, gradient
18
+ * card, media overlay). The light counterpart to the `overlay-*` scrims:
19
+ * those darken, this lightens. Deliberately not theme-tinted — it exists
20
+ * precisely for surfaces that stay dark in every theme, where a
21
+ * theme-relative surface token would resolve to white and give
22
+ * white-on-white. Pairs with on-primary (white) text. */
23
+ readonly frost: "bg-white/15";
17
24
  readonly "overlay-xs": "bg-black/40";
18
25
  readonly "overlay-sm": "bg-black/50";
19
26
  readonly "overlay-md": "bg-black/55";
@@ -27,6 +27,13 @@ export const SURFACE_MAP = {
27
27
  // Dark overlay scrim presets — used for image dimming, modal backdrops,
28
28
  // hover overlays. Backdrop-blur should be paired separately via className
29
29
  // when needed (backdrop-blur is not a colour utility, not in audit scope).
30
+ /** Translucent WHITE chip on a dark/branded backdrop (hero banner, gradient
31
+ * card, media overlay). The light counterpart to the `overlay-*` scrims:
32
+ * those darken, this lightens. Deliberately not theme-tinted — it exists
33
+ * precisely for surfaces that stay dark in every theme, where a
34
+ * theme-relative surface token would resolve to white and give
35
+ * white-on-white. Pairs with on-primary (white) text. */
36
+ frost: "bg-white/15",
30
37
  "overlay-xs": "bg-black/40",
31
38
  "overlay-sm": "bg-black/50",
32
39
  "overlay-md": "bg-black/55",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.12.0",
3
+ "version": "4.12.2",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -1,15 +0,0 @@
1
- import type { JobContext } from "../runtime/types";
2
- export interface NewBid {
3
- id: string;
4
- productId: string;
5
- productTitle: string;
6
- userId: string;
7
- userName: string;
8
- bidAmount: number;
9
- currency: string;
10
- }
11
- export interface HandleBidPlacedInput {
12
- bidId: string;
13
- bid: NewBid;
14
- }
15
- export declare function handleBidPlaced(input: HandleBidPlacedInput, ctx: JobContext): Promise<void>;
@@ -1,63 +0,0 @@
1
- import { normalizeError } from "../../../../errors/normalize";
2
- import { bidRepository, productRepository, } from "../../../../repositories";
3
- import { decryptPii } from "../../../../security/index";
4
- import { sendNotification } from "../../../../features/admin/actions/notification-actions";
5
- import { getAdminRealtimeDb } from "../../../../providers/db-firebase";
6
- import { BID_MESSAGES } from "../handlers/messages";
7
- const BIDS_COLLECTION = "bids";
8
- export async function handleBidPlaced(input, ctx) {
9
- const { bidId, bid: newBid } = input;
10
- newBid.userName = decryptPii(newBid.userName);
11
- try {
12
- const prevWinning = await bidRepository.getWinningBid(newBid.productId);
13
- const batch = ctx.db.batch();
14
- const newBidRef = ctx.db.collection(BIDS_COLLECTION).doc(bidId);
15
- bidRepository.markWinning(batch, newBidRef);
16
- let outbidUserId = null;
17
- if (prevWinning && prevWinning.data.userId !== newBid.userId) {
18
- outbidUserId = prevWinning.data.userId;
19
- bidRepository.markOutbid(batch, prevWinning.ref);
20
- }
21
- productRepository.incrementBidCountInBatch(batch, newBid.productId, newBid.bidAmount, newBid.userId);
22
- await batch.commit();
23
- if (outbidUserId) {
24
- const outbidMessage = BID_MESSAGES.OUTBID_MESSAGE(newBid.productTitle, newBid.currency, newBid.bidAmount);
25
- await sendNotification({
26
- userId: outbidUserId,
27
- type: "bid_outbid",
28
- priority: "high",
29
- title: BID_MESSAGES.OUTBID_TITLE,
30
- message: outbidMessage,
31
- relatedId: newBid.productId,
32
- relatedType: "product",
33
- });
34
- try {
35
- await getAdminRealtimeDb()
36
- .ref(`notifications/${outbidUserId}`)
37
- .push({
38
- type: "bid_outbid",
39
- title: BID_MESSAGES.OUTBID_TITLE,
40
- message: outbidMessage,
41
- timestamp: Date.now(),
42
- read: false,
43
- });
44
- }
45
- catch (rtdbError) {
46
- void normalizeError(rtdbError);
47
- ctx.logger.error("Realtime DB push failed (non-fatal)", rtdbError);
48
- }
49
- }
50
- ctx.logger.info(`Bid placed on ${newBid.productId}`, {
51
- bidId,
52
- amount: newBid.bidAmount,
53
- outbidUserId,
54
- });
55
- }
56
- catch (error) {
57
- ctx.logger.error("Error handling bid placement", error, {
58
- bidId,
59
- productId: newBid.productId,
60
- });
61
- throw error;
62
- }
63
- }
@@ -1,3 +0,0 @@
1
- import type { FirestoreTriggerHandler } from "../runtime/types";
2
- import { type NewBid } from "../core/onBidPlaced";
3
- export declare const onBidPlacedHandler: FirestoreTriggerHandler<null, NewBid>;
@@ -1,9 +0,0 @@
1
- import { handleBidPlaced } from "../core/onBidPlaced";
2
- export const onBidPlacedHandler = async (event, ctx) => {
3
- const newBid = event.after;
4
- if (!newBid) {
5
- ctx.logger.error("No snapshot data", null);
6
- return;
7
- }
8
- await handleBidPlaced({ bidId: event.params.bidId, bid: newBid }, ctx);
9
- };