@mohasinac/appkit 2.7.48 → 2.7.50

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 (110) hide show
  1. package/dist/_internal/client/features/layout/SidebarCollapseToggle.d.ts +19 -0
  2. package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +15 -0
  3. package/dist/_internal/client/features/layout/index.d.ts +2 -0
  4. package/dist/_internal/client/features/layout/index.js +1 -0
  5. package/dist/_internal/server/features/checkout/actions.js +25 -2
  6. package/dist/_internal/server/features/promotions/actions.js +3 -1
  7. package/dist/_internal/shared/actions/action-registry.js +149 -0
  8. package/dist/client.d.ts +18 -0
  9. package/dist/client.js +13 -0
  10. package/dist/features/account/components/UserSidebar.js +2 -1
  11. package/dist/features/admin/components/AdminBlogEditorView.js +6 -2
  12. package/dist/features/admin/components/AdminSidebar.js +2 -1
  13. package/dist/features/auth/repository/session.repository.js +16 -1
  14. package/dist/features/cart/hooks/useAddToCart.d.ts +2 -0
  15. package/dist/features/cart/hooks/useAddToCart.js +2 -0
  16. package/dist/features/cart/hooks/useGuestCart.d.ts +2 -0
  17. package/dist/features/cart/utils/guest-cart.d.ts +7 -0
  18. package/dist/features/cart/utils/pending-ops.d.ts +6 -0
  19. package/dist/features/categories/components/CategoryBundlesListing.js +9 -1
  20. package/dist/features/categories/components/CategoryProductsListing.js +6 -10
  21. package/dist/features/categories/schemas/firestore.d.ts +9 -0
  22. package/dist/features/events/components/EventRaffleWinnerView.d.ts +9 -1
  23. package/dist/features/events/components/EventRaffleWinnerView.js +3 -3
  24. package/dist/features/events/components/SpinWheelView.js +3 -3
  25. package/dist/features/layout/BottomNavLayout.d.ts +5 -0
  26. package/dist/features/layout/BottomNavLayout.js +21 -0
  27. package/dist/features/layout/TitleBar.js +7 -1
  28. package/dist/features/layout/TitleBarLayout.d.ts +8 -2
  29. package/dist/features/layout/TitleBarLayout.js +11 -7
  30. package/dist/features/orders/schemas/index.d.ts +2 -2
  31. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +9 -5
  32. package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
  33. package/dist/features/products/components/PrizeDrawEntryActions.d.ts +3 -1
  34. package/dist/features/products/components/PrizeDrawEntryActions.js +8 -12
  35. package/dist/features/products/components/ProductDetailActions.d.ts +5 -1
  36. package/dist/features/products/components/ProductDetailActions.js +3 -1
  37. package/dist/features/products/components/ProductDetailPageView.d.ts +2 -0
  38. package/dist/features/products/components/ProductDetailPageView.js +4 -0
  39. package/dist/features/products/components/ProductGrid.js +3 -3
  40. package/dist/features/products/components/ProductsIndexListing.js +15 -8
  41. package/dist/features/promotions/components/CouponCard.d.ts +11 -1
  42. package/dist/features/promotions/components/CouponCard.js +36 -2
  43. package/dist/features/promotions/repository/claimed-coupons.repository.d.ts +27 -0
  44. package/dist/features/promotions/repository/claimed-coupons.repository.js +115 -0
  45. package/dist/features/promotions/schemas/firestore.d.ts +34 -0
  46. package/dist/features/promotions/schemas/firestore.js +13 -0
  47. package/dist/features/seller/components/SellerAddressesView.js +3 -2
  48. package/dist/features/seller/components/SellerBidsView.js +33 -3
  49. package/dist/features/seller/components/SellerOrdersView.js +35 -0
  50. package/dist/features/seller/components/SellerProductShell.d.ts +2 -0
  51. package/dist/features/seller/components/SellerProductShell.js +26 -4
  52. package/dist/features/seller/components/SellerProductsCards.d.ts +19 -0
  53. package/dist/features/seller/components/SellerProductsCards.js +19 -0
  54. package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +14 -0
  55. package/dist/features/seller/components/SellerProductsFilterDrawer.js +20 -0
  56. package/dist/features/seller/components/SellerProductsView.d.ts +3 -1
  57. package/dist/features/seller/components/SellerProductsView.js +68 -19
  58. package/dist/features/seller/components/SellerReviewsView.js +67 -4
  59. package/dist/features/seller/components/SellerSidebar.js +2 -1
  60. package/dist/features/seller/components/seller-products-styles.d.ts +7 -0
  61. package/dist/features/seller/components/seller-products-styles.js +14 -0
  62. package/dist/features/shell/FormShell.d.ts +7 -1
  63. package/dist/features/shell/FormShell.js +5 -2
  64. package/dist/features/store-extensions/index.d.ts +4 -0
  65. package/dist/features/store-extensions/index.js +4 -0
  66. package/dist/features/store-extensions/repository/rbac.repositories.d.ts +23 -0
  67. package/dist/features/store-extensions/repository/rbac.repositories.js +32 -0
  68. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +91 -0
  69. package/dist/features/store-extensions/repository/store-extensions.repositories.js +127 -0
  70. package/dist/features/store-extensions/schemas/firestore.d.ts +244 -0
  71. package/dist/features/store-extensions/schemas/firestore.js +158 -0
  72. package/dist/features/store-extensions/schemas/rbac.d.ts +65 -0
  73. package/dist/features/store-extensions/schemas/rbac.js +43 -0
  74. package/dist/features/stores/components/InteractiveStoreCard.js +7 -3
  75. package/dist/features/stores/components/StoreProductsListing.js +6 -10
  76. package/dist/features/stores/schemas/firestore.d.ts +2 -0
  77. package/dist/index.d.ts +6 -0
  78. package/dist/index.js +8 -0
  79. package/dist/next/routing/route-map.d.ts +79 -0
  80. package/dist/next/routing/route-map.js +34 -0
  81. package/dist/react/hooks/useFormStatePreservation.d.ts +17 -0
  82. package/dist/react/hooks/useFormStatePreservation.js +62 -0
  83. package/dist/react/hooks/useInlineRowEdit.d.ts +24 -0
  84. package/dist/react/hooks/useInlineRowEdit.js +68 -0
  85. package/dist/react/index.d.ts +4 -0
  86. package/dist/react/index.js +4 -0
  87. package/dist/repositories/index.d.ts +4 -0
  88. package/dist/repositories/index.js +5 -0
  89. package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
  90. package/dist/seed/claimed-coupons-seed-data.d.ts +13 -0
  91. package/dist/seed/claimed-coupons-seed-data.js +79 -0
  92. package/dist/seed/index.d.ts +2 -0
  93. package/dist/seed/index.js +3 -0
  94. package/dist/seed/manifest.js +17 -0
  95. package/dist/seed/store-extensions-seed-data.d.ts +19 -0
  96. package/dist/seed/store-extensions-seed-data.js +421 -0
  97. package/dist/tailwind-utilities.css +1 -1
  98. package/dist/ui/components/Button.style.css +20 -12
  99. package/dist/ui/components/ClaimCouponButton.d.ts +28 -0
  100. package/dist/ui/components/ClaimCouponButton.js +89 -0
  101. package/dist/ui/components/QuickCreateModal.d.ts +15 -0
  102. package/dist/ui/components/QuickCreateModal.js +48 -0
  103. package/dist/ui/components/QuickCreateModal.style.css +84 -0
  104. package/dist/ui/components/StickyBottomBar.d.ts +22 -0
  105. package/dist/ui/components/StickyBottomBar.js +20 -0
  106. package/dist/ui/components/VacationBanner.d.ts +11 -0
  107. package/dist/ui/components/VacationBanner.js +16 -0
  108. package/dist/ui/index.d.ts +8 -0
  109. package/dist/ui/index.js +4 -0
  110. package/package.json +1 -1
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared collapse/expand handle rendered on the right edge of every dashboard
3
+ * sidebar (admin, store/seller, user). Replaces three identical inline copies
4
+ * that previously baked hardcoded hex gradients per role — see plan §1.
5
+ *
6
+ * Background uses `--appkit-color-primary-*` / `--appkit-color-secondary-*`
7
+ * tokens so the handle re-themes correctly in light and dark modes. Chevron
8
+ * foreground is pinned to `--appkit-color-text-on-primary` so it stays
9
+ * readable against the gradient regardless of theme.
10
+ */
11
+ import React from "react";
12
+ export interface SidebarCollapseToggleProps {
13
+ expanded: boolean;
14
+ onToggle: () => void;
15
+ /** aria-label override; defaults to "Collapse sidebar" / "Expand sidebar". */
16
+ expandedLabel?: string;
17
+ collapsedLabel?: string;
18
+ }
19
+ export declare function SidebarCollapseToggle({ expanded, onToggle, expandedLabel, collapsedLabel, }: SidebarCollapseToggleProps): React.JSX.Element;
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ const HANDLE_STYLE = {
4
+ background: "linear-gradient(to bottom, var(--appkit-color-primary-700, var(--appkit-color-primary)), var(--appkit-color-secondary-500, var(--appkit-color-secondary)))",
5
+ };
6
+ const CHEVRON_STYLE = {
7
+ color: "var(--appkit-color-text-on-primary, #ffffff)",
8
+ };
9
+ export function SidebarCollapseToggle({ expanded, onToggle, expandedLabel = "Collapse sidebar", collapsedLabel = "Expand sidebar", }) {
10
+ return (_jsx("button", { type: "button", onClick: onToggle, "aria-label": expanded ? expandedLabel : collapsedLabel, "aria-expanded": expanded,
11
+ // eslint-disable-next-line lir/no-inline-static-style
12
+ style: HANDLE_STYLE, className: "w-9 shrink-0 flex items-center justify-center cursor-pointer rounded-r-[1.25rem] shadow-lg transition-all duration-200 hover:shadow-xl hover:brightness-110 active:scale-[0.96]", children: _jsx("svg", { className: `w-4 h-4 drop-shadow-sm transition-transform duration-300 ${expanded ? "" : "rotate-180"}`,
13
+ // eslint-disable-next-line lir/no-inline-static-style
14
+ style: CHEVRON_STYLE, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }));
15
+ }
@@ -3,3 +3,5 @@ export { DashboardLayoutClient } from "./DashboardLayoutClient";
3
3
  export type { DashboardLayoutClientProps } from "./DashboardLayoutClient";
4
4
  export { RoleGuard } from "./RoleGuard";
5
5
  export type { RoleGuardProps } from "./RoleGuard";
6
+ export { SidebarCollapseToggle } from "./SidebarCollapseToggle";
7
+ export type { SidebarCollapseToggleProps } from "./SidebarCollapseToggle";
@@ -2,3 +2,4 @@
2
2
  /** Layout feature — client islands barrel. */
3
3
  export { DashboardLayoutClient } from "./DashboardLayoutClient";
4
4
  export { RoleGuard } from "./RoleGuard";
5
+ export { SidebarCollapseToggle } from "./SidebarCollapseToggle";
@@ -8,7 +8,7 @@
8
8
  */
9
9
  import { ApiError, ValidationError, NotFoundError, ERROR_MESSAGES } from "../../../../errors";
10
10
  import { serverLogger } from "../../../../monitoring";
11
- import { unitOfWork, siteSettingsRepository, userRepository, storeRepository, couponsRepository, notificationRepository, } from "../../../../repositories";
11
+ import { unitOfWork, siteSettingsRepository, userRepository, storeRepository, couponsRepository, notificationRepository, claimedCouponsRepository, } from "../../../../repositories";
12
12
  import { failedCheckoutRepository } from "../../../../features/checkout/repository/failed-checkout.repository";
13
13
  import { sendOrderConfirmationEmail } from "../../../../features/contact/server";
14
14
  import { splitCartIntoOrderGroups } from "../../../../features/orders/index";
@@ -549,7 +549,30 @@ export async function createCheckoutOrderAction(input) {
549
549
  });
550
550
  }
551
551
  if (couponUsageAccumulator.size > 0) {
552
- Promise.all([...couponUsageAccumulator.values()].map(({ couponId, code, orderIds: ids, totalDiscount }) => couponsRepository.applyCoupon(couponId, code, uid, ids, totalDiscount))).catch((err) => serverLogger.error("Failed to record coupon usage:", err));
552
+ Promise.all([...couponUsageAccumulator.values()].map(async ({ couponId, code, orderIds: ids, totalDiscount }) => {
553
+ await couponsRepository.applyCoupon(couponId, code, uid, ids, totalDiscount);
554
+ // Flip the user's claimed-coupon wallet row to "used" when the
555
+ // per-user redemption limit has been reached (default: single-use
556
+ // when `perUserLimit` is absent or 1). Multi-use coupons with
557
+ // remaining redemptions stay "active" in the wallet.
558
+ try {
559
+ const coupon = await couponsRepository.findById(couponId);
560
+ // Default to single-use (1) when the coupon has no explicit
561
+ // per-user cap configured. Coupons issued via the wallet are
562
+ // claim-once-use-once by design unless the seller opts into a
563
+ // higher cap.
564
+ const perUserLimit = coupon?.usage?.perUserLimit ?? 1;
565
+ const newCount = await couponsRepository.getUserCouponUsageCount(uid, couponId);
566
+ if (newCount >= perUserLimit) {
567
+ await claimedCouponsRepository
568
+ .markUsed(uid, code, ids[0] ?? "")
569
+ .catch(() => { });
570
+ }
571
+ }
572
+ catch (err) {
573
+ serverLogger.warn("Failed to update claimed-coupon status", { err });
574
+ }
575
+ })).catch((err) => serverLogger.error("Failed to record coupon usage:", err));
553
576
  }
554
577
  if (emailOtpUsed && unavailable.length > 0) {
555
578
  const metaRef = consentOtpRateLimitRef(db, uid);
@@ -13,7 +13,9 @@ export async function applyCouponAction(input) {
13
13
  throw new ValidationError(parsed.error.issues[0]?.message ?? ERR_INVALID_COUPON_INPUT);
14
14
  const coupon = await validateCoupon(parsed.data, user.uid);
15
15
  const discount = computeDiscount(coupon, parsed.data.cartTotal);
16
- await couponsRepository.applyCoupon(coupon.id, coupon.code, user.uid, [], discount);
16
+ // Coupon usage is recorded ONLY after order confirmation
17
+ // (see `_internal/server/features/checkout/actions.ts`). Validating /
18
+ // applying a coupon to a cart must never consume a redemption.
17
19
  return { couponId: coupon.id, code: coupon.code, discount, type: coupon.type };
18
20
  }
19
21
  export async function createCouponAction(input) {
@@ -337,6 +337,105 @@ export const ACTIONS = {
337
337
  kind: "ghost",
338
338
  permissions: ["seller", "admin"],
339
339
  },
340
+ // SB-UNI-W follow-up (plan §7) — leaves for the 11 unwired store-dashboard
341
+ // form/row CTAs identified by the W-3 sweep.
342
+ "create-template": {
343
+ id: "store.create-template",
344
+ label: "Create Template",
345
+ ariaLabel: "Create a new product template",
346
+ description: "Submit the new-template form.",
347
+ kind: "primary",
348
+ permissions: ["seller", "admin"],
349
+ },
350
+ "update-template": {
351
+ id: "store.update-template",
352
+ label: "Save Changes",
353
+ ariaLabel: "Save changes to template",
354
+ description: "Submit the edit-template form.",
355
+ kind: "primary",
356
+ permissions: ["seller", "admin"],
357
+ },
358
+ "edit-template": {
359
+ id: "store.edit-template",
360
+ label: "Edit",
361
+ ariaLabel: "Edit template",
362
+ description: "Open the edit-template drawer for this row.",
363
+ kind: "ghost",
364
+ permissions: ["seller", "admin"],
365
+ },
366
+ "delete-template": {
367
+ id: "store.delete-template",
368
+ label: "Delete",
369
+ ariaLabel: "Delete template",
370
+ description: "Permanently remove this template.",
371
+ kind: "danger",
372
+ permissions: ["seller", "admin"],
373
+ confirmation: {
374
+ title: "Delete template?",
375
+ body: "This template will be permanently removed. Existing products created from it are unaffected. This action cannot be undone.",
376
+ confirmLabel: "Delete template",
377
+ confirmKind: "danger",
378
+ },
379
+ },
380
+ "create-bundle": {
381
+ id: "store.create-bundle",
382
+ label: "Create Bundle",
383
+ ariaLabel: "Create a new bundle",
384
+ description: "Submit the new-bundle form.",
385
+ kind: "primary",
386
+ permissions: ["seller", "admin"],
387
+ },
388
+ "create-feature": {
389
+ id: "store.create-feature",
390
+ label: "Create Feature",
391
+ ariaLabel: "Create a new product feature badge",
392
+ description: "Submit the new-feature form.",
393
+ kind: "primary",
394
+ permissions: ["seller", "admin"],
395
+ },
396
+ "update-slug": {
397
+ id: "store.update-slug",
398
+ label: "Update Slug",
399
+ ariaLabel: "Update store slug",
400
+ description: "Submit the slug-change form. Caution: existing links will redirect.",
401
+ kind: "primary",
402
+ permissions: ["seller", "admin"],
403
+ confirmation: {
404
+ title: "Update store slug?",
405
+ body: "Existing links that reference the old slug will start redirecting. Make sure to update social bios and external listings.",
406
+ confirmLabel: "Update slug",
407
+ confirmKind: "primary",
408
+ },
409
+ },
410
+ "delete-sublisting-category": {
411
+ id: "store.delete-sublisting-category",
412
+ label: "Delete",
413
+ ariaLabel: "Delete sublisting category",
414
+ description: "Permanently remove this sublisting category from the store.",
415
+ kind: "danger",
416
+ permissions: ["seller", "admin"],
417
+ confirmation: {
418
+ title: "Delete sublisting category?",
419
+ body: "Products tagged with this category will lose the tag. This action cannot be undone.",
420
+ confirmLabel: "Delete category",
421
+ confirmKind: "danger",
422
+ },
423
+ },
424
+ "cancel-form": {
425
+ id: "store.cancel-form",
426
+ label: "Cancel",
427
+ ariaLabel: "Cancel form and close",
428
+ description: "Discard unsaved changes and close the drawer/form.",
429
+ kind: "ghost",
430
+ },
431
+ "new-template": {
432
+ id: "store.new-template",
433
+ label: "+ New Template",
434
+ ariaLabel: "Open the new-template drawer",
435
+ description: "Open the new-template creation drawer.",
436
+ kind: "primary",
437
+ permissions: ["seller", "admin"],
438
+ },
340
439
  },
341
440
  BLOG: {},
342
441
  EVENT: {
@@ -416,6 +515,56 @@ export const ACTIONS = {
416
515
  description: "Mark this address as the default delivery address.",
417
516
  kind: "ghost",
418
517
  },
518
+ // Plan §6 — wishlist bulk actions (sticky bulk-action toolbar leaves).
519
+ "wishlist-bulk-remove": {
520
+ id: "user.wishlist-bulk-remove",
521
+ label: "Remove selected",
522
+ ariaLabel: "Remove selected items from wishlist",
523
+ description: "Permanently remove the selected items from your wishlist.",
524
+ kind: "danger",
525
+ confirmation: {
526
+ title: "Remove selected items?",
527
+ body: "The selected items will be removed from your wishlist. You can add them again from product pages.",
528
+ confirmLabel: "Remove",
529
+ confirmKind: "danger",
530
+ },
531
+ },
532
+ "wishlist-bulk-move-to-cart": {
533
+ id: "user.wishlist-bulk-move-to-cart",
534
+ label: "Move to cart",
535
+ ariaLabel: "Move selected items to cart",
536
+ description: "Add the selected wishlist items to your cart.",
537
+ kind: "primary",
538
+ },
539
+ "clear-selection": {
540
+ id: "user.clear-selection",
541
+ label: "Deselect",
542
+ ariaLabel: "Clear current selection",
543
+ description: "Exit selection mode.",
544
+ kind: "ghost",
545
+ },
546
+ // Plan §10 — claim-coupon wallet (won-coupon entry points & wallet leaves).
547
+ "claim-coupon": {
548
+ id: "user.claim-coupon",
549
+ label: "Claim Coupon",
550
+ ariaLabel: "Claim this coupon to your wallet",
551
+ description: "Add this coupon to your claimed-coupons wallet for use at checkout.",
552
+ kind: "primary",
553
+ },
554
+ "use-claimed-coupon": {
555
+ id: "user.use-claimed-coupon",
556
+ label: "Apply at checkout",
557
+ ariaLabel: "Apply this coupon at checkout",
558
+ description: "Deep-link to checkout with this coupon pre-filled.",
559
+ kind: "link",
560
+ },
561
+ "remove-claimed-coupon": {
562
+ id: "user.remove-claimed-coupon",
563
+ label: "Remove",
564
+ ariaLabel: "Remove coupon from wallet",
565
+ description: "Remove this coupon from your wallet (history is preserved).",
566
+ kind: "ghost",
567
+ },
419
568
  },
420
569
  SELLER: {},
421
570
  ADMIN: {
package/dist/client.d.ts CHANGED
@@ -2,9 +2,21 @@ export { registerClientAuthProvider, getClientAuthProvider, type IClientAuthProv
2
2
  export { registerClientRealtimeProvider, getClientRealtimeProvider, type IClientRealtimeProvider, type RealtimeSnapshot, type Unsubscribe, } from "./contracts/client-realtime";
3
3
  export { registerClientSessionAdapter, getClientSessionAdapter, type IClientSessionAdapter, type AdapterAuthUser, type AuthUnsubscribe, } from "./contracts/client-session";
4
4
  export { ConfirmDeleteModal } from "./ui/components/ConfirmDeleteModal";
5
+ export { EmptyState } from "./ui/components/EmptyState";
6
+ export { Divider } from "./ui/components/Divider";
7
+ export { TabStrip } from "./ui/components/TabStrip";
8
+ export { SellerProductsView } from "./features/seller/components/SellerProductsView";
5
9
  export { Drawer } from "./ui/components/Drawer";
6
10
  export { FilterDrawer } from "./ui/components/FilterDrawer";
7
11
  export { Modal } from "./ui/components/Modal";
12
+ export { QuickCreateModal } from "./ui/components/QuickCreateModal";
13
+ export type { QuickCreateModalProps } from "./ui/components/QuickCreateModal";
14
+ export { VacationBanner } from "./ui/components/VacationBanner";
15
+ export type { VacationBannerProps } from "./ui/components/VacationBanner";
16
+ export { useInlineToggle, useInlineTextEdit } from "./react/hooks/useInlineRowEdit";
17
+ export type { InlineEditOptions, InlineToggleResult, InlineTextResult, } from "./react/hooks/useInlineRowEdit";
18
+ export { useFormStatePreservation } from "./react/hooks/useFormStatePreservation";
19
+ export type { FormStatePreservationOptions } from "./react/hooks/useFormStatePreservation";
8
20
  export { SideDrawer } from "./ui/components/SideDrawer";
9
21
  export { SideModal } from "./ui/components/SideModal";
10
22
  export { UnsavedChangesModal } from "./ui/components/UnsavedChangesModal";
@@ -51,6 +63,10 @@ export { Div } from "./ui/components/Div";
51
63
  export { Main, Section } from "./ui/components/Semantic";
52
64
  export { Badge } from "./ui/components/Badge";
53
65
  export { Button } from "./ui/components/Button";
66
+ export { StickyBottomBar } from "./ui/components/StickyBottomBar";
67
+ export type { StickyBottomBarProps } from "./ui/components/StickyBottomBar";
68
+ export { ClaimCouponButton } from "./ui/components/ClaimCouponButton";
69
+ export type { ClaimCouponButtonProps } from "./ui/components/ClaimCouponButton";
54
70
  export { Checkbox } from "./ui/components/Checkbox";
55
71
  export { Input } from "./ui/components/Input";
56
72
  export { OtpInput } from "./ui/components/OtpInput";
@@ -230,3 +246,5 @@ export { LiveItemDetailView } from "./_internal/client/features/live/LiveItemDet
230
246
  export type { LiveItemDetailViewProps } from "./_internal/client/features/live/LiveItemDetailView";
231
247
  export { PhysicalLocationModal } from "./features/seller/components/PhysicalLocationModal";
232
248
  export type { PhysicalLocation } from "./features/seller/components/PhysicalLocationModal";
249
+ export { Toggle } from "./ui/components/Toggle";
250
+ export type { ToggleProps } from "./ui/components/Toggle";
package/dist/client.js CHANGED
@@ -9,6 +9,11 @@ export { registerClientSessionAdapter, getClientSessionAdapter, } from "./contra
9
9
  // [CLIENT-ONLY]-Cannot run in SSR mode â€" uses browser-only APIs (window, navigator, localStorage, matchMedia, DOM events) that do not exist in Node.js.
10
10
  // ConfirmDeleteModal - Component for confirm delete modal.
11
11
  export { ConfirmDeleteModal } from "./ui/components/ConfirmDeleteModal";
12
+ // S-STORE — pure-UI primitives + Seller view consumed by new dashboard pages
13
+ export { EmptyState } from "./ui/components/EmptyState";
14
+ export { Divider } from "./ui/components/Divider";
15
+ export { TabStrip } from "./ui/components/TabStrip";
16
+ export { SellerProductsView } from "./features/seller/components/SellerProductsView";
12
17
  // [CLIENT-ONLY]-Cannot run in SSR mode â€" uses browser-only APIs (window, navigator, localStorage, matchMedia, DOM events) that do not exist in Node.js.
13
18
  // Drawer - Component for drawer.
14
19
  export { Drawer } from "./ui/components/Drawer";
@@ -18,6 +23,10 @@ export { FilterDrawer } from "./ui/components/FilterDrawer";
18
23
  // [CLIENT-ONLY]-Cannot run in SSR mode â€" uses browser-only APIs (window, navigator, localStorage, matchMedia, DOM events) that do not exist in Node.js.
19
24
  // Modal - Component for modal.
20
25
  export { Modal } from "./ui/components/Modal";
26
+ export { QuickCreateModal } from "./ui/components/QuickCreateModal";
27
+ export { VacationBanner } from "./ui/components/VacationBanner";
28
+ export { useInlineToggle, useInlineTextEdit } from "./react/hooks/useInlineRowEdit";
29
+ export { useFormStatePreservation } from "./react/hooks/useFormStatePreservation";
21
30
  // [CLIENT-ONLY]-Cannot run in SSR mode â€" uses browser-only APIs (window, navigator, localStorage, matchMedia, DOM events) that do not exist in Node.js.
22
31
  // SideDrawer - Component for side drawer.
23
32
  export { SideDrawer } from "./ui/components/SideDrawer";
@@ -119,6 +128,8 @@ export { Div } from "./ui/components/Div";
119
128
  export { Main, Section } from "./ui/components/Semantic";
120
129
  export { Badge } from "./ui/components/Badge";
121
130
  export { Button } from "./ui/components/Button";
131
+ export { StickyBottomBar } from "./ui/components/StickyBottomBar";
132
+ export { ClaimCouponButton } from "./ui/components/ClaimCouponButton";
122
133
  export { Checkbox } from "./ui/components/Checkbox";
123
134
  export { Input } from "./ui/components/Input";
124
135
  export { OtpInput } from "./ui/components/OtpInput";
@@ -245,3 +256,5 @@ export { DigitalCodeDetailView } from "./_internal/client/features/digital-code/
245
256
  export { CodeRevealPanel } from "./_internal/client/features/digital-code/CodeRevealPanel";
246
257
  export { LiveItemDetailView } from "./_internal/client/features/live/LiveItemDetailView";
247
258
  export { PhysicalLocationModal } from "./features/seller/components/PhysicalLocationModal";
259
+ // [CLIENT-ONLY] — Toggle switch primitive.
260
+ export { Toggle } from "./ui/components/Toggle";
@@ -6,6 +6,7 @@ import Link from "next/link";
6
6
  import { usePathname } from "next/navigation";
7
7
  import { Li, Nav, Span, Ul } from "../../../ui";
8
8
  import { BottomSheet } from "../../layout/BottomSheet";
9
+ import { SidebarCollapseToggle } from "../../../_internal/client/features/layout/SidebarCollapseToggle";
9
10
  function isNavItemActive(item, activeHref) {
10
11
  return activeHref === item.href || activeHref.startsWith(item.href + "/");
11
12
  }
@@ -73,7 +74,7 @@ export function UserSidebar({ items, groups, mobileOpen = false, onCloseMobile,
73
74
  height: "calc(100vh - var(--header-height, 3.5rem))",
74
75
  width: "13rem",
75
76
  transform: desktopOpen ? "translateX(0)" : "translateX(calc(-100% + 1.25rem))",
76
- }, children: [_jsxs("div", { className: "flex-1 bg-white dark:bg-slate-900 border-r border-zinc-200 dark:border-slate-800 flex flex-col overflow-hidden shadow-xl", children: [_jsx("div", { className: "px-4 py-3.5 border-b border-zinc-100 dark:border-slate-800 shrink-0", children: _jsx("span", { className: "text-xs font-semibold uppercase tracking-widest text-zinc-400 dark:text-zinc-500", children: "My Account" }) }), _jsx("div", { className: "flex-1 overflow-y-auto", children: _jsx(DrawerContent, { groups: groups, items: items, activeHref: pathname }) })] }), _jsx("button", { type: "button", onClick: handleToggle, "aria-label": desktopOpen ? "Collapse sidebar" : "Expand sidebar", className: "w-9 shrink-0 flex items-center justify-center cursor-pointer rounded-r-[1.25rem] shadow-lg transition-all duration-200 hover:shadow-xl hover:brightness-110 active:scale-[0.96]", style: { background: "linear-gradient(to bottom, var(--appkit-color-primary-700, var(--appkit-color-primary)), var(--appkit-color-secondary-500, var(--appkit-color-secondary)))" }, children: _jsx("svg", { className: `w-4 h-4 text-white drop-shadow-sm transition-transform duration-300 ${desktopOpen ? "" : "rotate-180"}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) })] }), _jsx("div", { className: "md:hidden", children: _jsx(BottomSheet, { open: mobileOpen, onClose: close, title: "My Account", children: _jsx(DrawerContent, { groups: groups, items: items, activeHref: pathname, onItemClick: close }) }) })] }));
77
+ }, children: [_jsxs("div", { className: "flex-1 bg-white dark:bg-slate-900 border-r border-zinc-200 dark:border-slate-800 flex flex-col overflow-hidden shadow-xl", children: [_jsx("div", { className: "px-4 py-3.5 border-b border-zinc-100 dark:border-slate-800 shrink-0", children: _jsx("span", { className: "text-xs font-semibold uppercase tracking-widest text-zinc-400 dark:text-zinc-500", children: "My Account" }) }), _jsx("div", { className: "flex-1 overflow-y-auto", children: _jsx(DrawerContent, { groups: groups, items: items, activeHref: pathname }) })] }), _jsx(SidebarCollapseToggle, { expanded: desktopOpen, onToggle: handleToggle })] }), _jsx("div", { className: "md:hidden", children: _jsx(BottomSheet, { open: mobileOpen, onClose: close, title: "My Account", children: _jsx(DrawerContent, { groups: groups, items: items, activeHref: pathname, onItemClick: close }) }) })] }));
77
78
  }
78
79
  // ── Overlay variant (default) — portal on desktop, bottom-sheet on mobile ─
79
80
  const content = (_jsx(DrawerContent, { groups: groups, items: items, activeHref: pathname, onItemClick: close }));
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import React from "react";
4
4
  import { useMutation, useQuery } from "@tanstack/react-query";
5
- import { Button, Form, Input, RichTextEditor, Select, StackedViewShell, TagInput, Text, Toggle, useToast } from "../../../ui";
5
+ import { Button, Form, Heading, Input, RichTextEditor, RichTextRenderer, Select, StackedViewShell, TagInput, Text, Toggle, useToast } from "../../../ui";
6
6
  import { FieldInput, FormShellContext, useFormShellState } from "../../../ui/forms";
7
7
  import { ImageUpload } from "../../media/upload/ImageUpload";
8
8
  import { useMediaUpload } from "../../media";
@@ -167,5 +167,9 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
167
167
  if (embedded) {
168
168
  return _jsx("div", { className: "overflow-y-auto p-4", children: formSection });
169
169
  }
170
- return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Post" : "New Blog Post", sections: [formSection] }));
170
+ // S-STORE-9B-D rendered preview pane alongside the editor.
171
+ const previewSection = (_jsxs("div", { className: "rounded-lg border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface-raised)] p-5 max-h-[calc(100vh-12rem)] overflow-y-auto", children: [_jsx("div", { className: "flex items-center gap-2 mb-3 text-xs font-semibold uppercase tracking-wide text-[var(--appkit-color-text-muted)]", children: _jsx("span", { children: "Live preview" }) }), coverImage ? (_jsx("img", { src: coverImage, alt: "", className: "w-full rounded-md mb-4 object-cover max-h-64" })) : null, _jsx(Heading, { level: 1, className: "text-2xl font-bold mb-1", children: title || "Untitled post" }), _jsxs(Text, { className: "text-xs text-[var(--appkit-color-text-muted)] mb-4", children: [authorName || "Anonymous", category ? ` · ${category}` : ""] }), excerpt ? (_jsx(Text, { className: "italic text-[var(--appkit-color-text-secondary)] mb-4", children: excerpt })) : null, _jsx(RichTextRenderer, { html: content || "<em>No content yet…</em>" }), tags.length > 0 ? (_jsx("div", { className: "mt-6 flex flex-wrap gap-1.5", children: tags.map((t) => (_jsxs("span", { className: "px-2 py-0.5 rounded-full bg-[var(--appkit-color-surface)] border border-[var(--appkit-color-border)] text-xs", children: ["#", t] }, t))) })) : null] }, "preview"));
172
+ return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: isEdit ? "Edit Post" : "New Blog Post", sections: [
173
+ _jsxs("div", { className: "lg:grid lg:grid-cols-[3fr_2fr] lg:gap-6", children: [_jsx("div", { children: formSection }), _jsx("div", { className: "mt-6 lg:mt-0 lg:sticky lg:top-4 lg:self-start", children: previewSection })] }, "split"),
174
+ ] }));
171
175
  }
@@ -5,6 +5,7 @@ import { createPortal } from "react-dom";
5
5
  import Link from "next/link";
6
6
  import { Nav } from "../../../ui";
7
7
  import { BottomSheet } from "../../layout/BottomSheet";
8
+ import { SidebarCollapseToggle } from "../../../_internal/client/features/layout/SidebarCollapseToggle";
8
9
  function isNavItemActive(item, activePath) {
9
10
  return activePath === item.href || activePath.startsWith(item.href + "/");
10
11
  }
@@ -47,7 +48,7 @@ export function AdminSidebar({ renderNavItems, renderHeader, renderFooter, activ
47
48
  height: "calc(100vh - var(--header-height, 3.5rem))",
48
49
  width: "18rem",
49
50
  transform: desktopOpen ? "translateX(0)" : "translateX(calc(-100% + 1.25rem))",
50
- }, children: [_jsxs("div", { className: "flex-1 bg-white dark:bg-slate-950 border-r border-zinc-200 dark:border-slate-800 flex flex-col overflow-hidden shadow-xl", children: [_jsx("div", { className: "px-4 py-3.5 border-b border-zinc-100 dark:border-slate-800 shrink-0", children: renderHeader ? renderHeader() : _jsx("span", { className: "text-xs font-semibold uppercase tracking-widest text-zinc-400 dark:text-zinc-500", children: "Admin Panel" }) }), _jsx("div", { className: "flex-1 overflow-y-auto", children: navContent }), renderFooter && _jsx("div", { className: "px-4 py-3 border-t border-zinc-200 dark:border-slate-800", children: renderFooter() })] }), _jsx("button", { type: "button", onClick: handleToggle, "aria-label": desktopOpen ? "Collapse sidebar" : "Expand sidebar", className: "w-9 shrink-0 flex items-center justify-center cursor-pointer rounded-r-[1.25rem] shadow-lg transition-all duration-200 hover:shadow-xl hover:brightness-110 active:scale-[0.96]", style: { background: "linear-gradient(to bottom, #1343de, #3570fc)" }, children: _jsx("svg", { className: `w-4 h-4 text-white drop-shadow-sm transition-transform duration-300 ${desktopOpen ? "" : "rotate-180"}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) })] }), _jsx("div", { className: "md:hidden", children: _jsx(BottomSheet, { open: mobileOpen, onClose: close, title: "Admin Panel", children: mobileNavContent }) })] }));
51
+ }, children: [_jsxs("div", { className: "flex-1 bg-white dark:bg-slate-950 border-r border-zinc-200 dark:border-slate-800 flex flex-col overflow-hidden shadow-xl", children: [_jsx("div", { className: "px-4 py-3.5 border-b border-zinc-100 dark:border-slate-800 shrink-0", children: renderHeader ? renderHeader() : _jsx("span", { className: "text-xs font-semibold uppercase tracking-widest text-zinc-400 dark:text-zinc-500", children: "Admin Panel" }) }), _jsx("div", { className: "flex-1 overflow-y-auto", children: navContent }), renderFooter && _jsx("div", { className: "px-4 py-3 border-t border-zinc-200 dark:border-slate-800", children: renderFooter() })] }), _jsx(SidebarCollapseToggle, { expanded: desktopOpen, onToggle: handleToggle })] }), _jsx("div", { className: "md:hidden", children: _jsx(BottomSheet, { open: mobileOpen, onClose: close, title: "Admin Panel", children: mobileNavContent }) })] }));
51
52
  }
52
53
  return (_jsxs(_Fragment, { children: [mounted && mobileOpen &&
53
54
  createPortal(_jsx(DrawerPanel, { title: "Admin Panel", onClose: close, children: navContent }), document.body), _jsx("div", { className: "md:hidden", children: _jsx(BottomSheet, { open: mobileOpen, onClose: close, title: "Admin Panel", children: mobileNavContent }) })] }));
@@ -35,7 +35,22 @@ export class SessionRepository extends BaseRepository {
35
35
  lastActivity: new Date(),
36
36
  ...(data?.location && { location: data.location }),
37
37
  };
38
- await this.update(sessionId, updateData);
38
+ try {
39
+ await this.update(sessionId, updateData);
40
+ }
41
+ catch (err) {
42
+ // The session doc may have been revoked/expired/wiped while the client
43
+ // still holds the cookie. Firestore Admin throws `5 NOT_FOUND` in that
44
+ // case — swallow it so the activity ping is best-effort, not a 500.
45
+ // Any other error is rethrown so genuine failures still surface.
46
+ const code = err?.code;
47
+ const msg = err?.message ?? "";
48
+ const isNotFound = code === 5 ||
49
+ code === "not-found" ||
50
+ /No document to update|NOT_FOUND/i.test(msg);
51
+ if (!isNotFound)
52
+ throw err;
53
+ }
39
54
  }
40
55
  async revokeSession(sessionId, revokedBy) {
41
56
  await this.update(sessionId, {
@@ -5,6 +5,8 @@ interface AddToCartPayload {
5
5
  productTitle?: string;
6
6
  productImage?: string;
7
7
  price?: number;
8
+ storeId?: string;
9
+ storeName?: string;
8
10
  [key: string]: unknown;
9
11
  }
10
12
  interface UseAddToCartOptions {
@@ -35,6 +35,8 @@ export function useAddToCart(options) {
35
35
  productTitle: data.productTitle,
36
36
  productImage: data.productImage,
37
37
  price: data.price,
38
+ storeId: data.storeId,
39
+ storeName: data.storeName,
38
40
  });
39
41
  optionsRef.current?.onSuccess?.();
40
42
  return undefined;
@@ -11,6 +11,8 @@ export declare function useGuestCart(): {
11
11
  productTitle?: string;
12
12
  productImage?: string;
13
13
  price?: number;
14
+ storeId?: string;
15
+ storeName?: string;
14
16
  }) => void;
15
17
  remove: (productId: string) => void;
16
18
  updateQuantity: (productId: string, quantity: number) => void;
@@ -10,6 +10,11 @@ export interface GuestCartItem {
10
10
  productTitle?: string;
11
11
  productImage?: string;
12
12
  price?: number;
13
+ /** Store identifier (storeSlug) — required to group cart lines by seller. */
14
+ storeId?: string;
15
+ /** Denormalised store display name — keeps the cart-group header readable
16
+ * for guest carts where we can't round-trip to Firestore at render time. */
17
+ storeName?: string;
13
18
  }
14
19
  export interface GuestCartStorage {
15
20
  getItem(key: string): string | null;
@@ -21,6 +26,8 @@ export declare function addToGuestCart(productId: string, quantity: number, snap
21
26
  productTitle?: string;
22
27
  productImage?: string;
23
28
  price?: number;
29
+ storeId?: string;
30
+ storeName?: string;
24
31
  }, storage?: GuestCartStorage | null, key?: string): GuestCartItem[];
25
32
  export declare function removeFromGuestCart(productId: string, storage?: GuestCartStorage | null, key?: string): GuestCartItem[];
26
33
  export declare function updateGuestCartQuantity(productId: string, quantity: number, storage?: GuestCartStorage | null, key?: string): GuestCartItem[];
@@ -15,6 +15,12 @@ export interface CartOp {
15
15
  productTitle?: string;
16
16
  productImage?: string;
17
17
  price?: number;
18
+ /** Store identifier (storeSlug) — carried into the server cart on sync so
19
+ * cart lines group by seller correctly for both guest + signed-in flows. */
20
+ storeId?: string;
21
+ /** Denormalised store display name — keeps the group header readable when
22
+ * the server-side hydration has not yet run. */
23
+ storeName?: string;
18
24
  ts: number;
19
25
  }
20
26
  export interface WishlistOp {
@@ -103,7 +103,15 @@ function BundleCard({ bundle, onBuyNow }) {
103
103
  const cover = bundle.display?.coverImage;
104
104
  const href = String(ROUTES.PUBLIC.BUNDLE_DETAIL?.(bundle.slug) ?? "#");
105
105
  const price = bundle.bundlePriceInPaise;
106
- return (_jsxs("div", { className: "flex flex-col rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900", children: [_jsxs(Link, { href: href, className: "group block flex-1 p-3 hover:no-underline", children: [_jsx(Div, { className: "mb-2 aspect-video overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800", children: cover ? (
106
+ // Plan §8 render a 2x2 collage of member product images when the bundle's
107
+ // bundleItemDetails carry denormalised imageURLs. Falls back to the single
108
+ // hero cover (or emoji placeholder) when fewer than 2 are available.
109
+ const collageTiles = (bundle.bundleItemDetails ?? [])
110
+ .filter((d) => Boolean(d.imageURL))
111
+ .slice(0, 4);
112
+ const showCollage = collageTiles.length >= 2;
113
+ const overflow = memberCount - collageTiles.length;
114
+ return (_jsxs("div", { className: "flex flex-col rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900", children: [_jsxs(Link, { href: href, className: "group block flex-1 p-3 hover:no-underline", children: [_jsx(Div, { className: "mb-2 aspect-video overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800", children: showCollage ? (_jsx(Div, { className: `grid h-full w-full gap-0.5 ${collageTiles.length === 2 ? "grid-cols-2 grid-rows-1" : "grid-cols-2 grid-rows-2"}`, children: collageTiles.map((tile, i) => (_jsxs(Div, { className: "relative overflow-hidden bg-zinc-50 dark:bg-zinc-800", children: [_jsx("img", { src: tile.imageURL, alt: tile.title ?? `${bundle.name} item ${i + 1}`, className: "h-full w-full object-cover transition-transform group-hover:scale-105", loading: "lazy" }), i === collageTiles.length - 1 && overflow > 0 && (_jsxs(Div, { className: "absolute inset-0 flex items-center justify-center bg-black/55 text-sm font-semibold text-white", children: ["+", overflow] }))] }, `${tile.productId}-${i}`))) })) : cover ? (
107
115
  // eslint-disable-next-line @next/next/no-img-element
108
116
  _jsx("img", { src: cover, alt: bundle.name, className: "h-full w-full object-cover transition-transform group-hover:scale-105", loading: "lazy" })) : (_jsx(Div, { className: "flex h-full w-full items-center justify-center text-3xl", children: PLACEHOLDER_EMOJI })) }), _jsx(Text, { className: "line-clamp-2 text-sm font-semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", children: [_jsx(Text, { size: "sm", weight: "bold", children: price ? formatCurrency(price / 100, "INR") : "—" }), _jsxs(Text, { size: "xs", color: "muted", children: [memberCount, " item", memberCount !== 1 ? "s" : ""] })] }), badge && (_jsx(Badge, { variant: STOCK_BADGE_VARIANT[stock], className: "mt-1", children: badge }))] }), onBuyNow && (_jsx(Div, { className: "border-t border-zinc-100 p-3 pt-2 dark:border-zinc-800", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }));
109
117
  }
@@ -85,19 +85,15 @@ export function CategoryProductsListing({ categorySlug, categoryId, brandName, i
85
85
  });
86
86
  }, [wishlistedIds, localWishlist, showToast, requireAuth]);
87
87
  const handleAddToCart = useCallback((product) => {
88
- localCart.add(product.id, 1, {
88
+ const snapshot = {
89
89
  productTitle: product.title,
90
90
  productImage: product.mainImage,
91
91
  price: product.price,
92
- });
93
- pushCartOp({
94
- op: "add",
95
- productId: product.id,
96
- quantity: 1,
97
- productTitle: product.title,
98
- productImage: product.mainImage,
99
- price: product.price,
100
- });
92
+ storeId: product.storeId,
93
+ storeName: product.storeName,
94
+ };
95
+ localCart.add(product.id, 1, snapshot);
96
+ pushCartOp({ op: "add", productId: product.id, quantity: 1, ...snapshot });
101
97
  showToast("Added to cart", "success");
102
98
  }, [localCart, showToast]);
103
99
  return (_jsxs("div", { className: "min-h-[200px]", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: `Search in ${categorySlug.replace(/-/g, " ")}...`, onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || "-createdAt", sortOptions: PRODUCT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => {
@@ -44,6 +44,15 @@ export interface BundleItemDetail {
44
44
  productId: string;
45
45
  /** Number of draw entries included when product is a prize-draw. */
46
46
  drawCount?: number;
47
+ /**
48
+ * Denormalised member product image URL — snapshotted at bundle-edit time
49
+ * by the admin/store bundle editor (or the `onProductWrite` fan-out) so the
50
+ * BundleCard can render a 2x2 collage without N+1 product fetches.
51
+ * When absent the card falls back to `display.coverImage`.
52
+ */
53
+ imageURL?: string;
54
+ /** Denormalised member product title — used as alt text for the collage tile. */
55
+ title?: string;
47
56
  }
48
57
  /** Full Firestore category document (includes server-only fields) */
49
58
  export interface CategoryDocumentSEO {
@@ -5,9 +5,17 @@ export interface RaffleWinnerEvent {
5
5
  raffleEntryCount?: number;
6
6
  raffleTriggeredAt?: string | Date;
7
7
  raffleGithubFunctionUrl?: string;
8
+ /**
9
+ * Plan §10 — when the prize is a coupon, the issuing code so the winner
10
+ * can claim it directly via the ClaimCouponButton. Pair with
11
+ * `currentUserIsWinner` so the CTA only renders for the actual winner.
12
+ */
13
+ rafflePrizeCouponCode?: string;
8
14
  }
9
15
  export interface EventRaffleWinnerViewProps {
10
16
  event: RaffleWinnerEvent;
17
+ /** When true (= page resolver matched the viewer to raffleWinnerUserId), show the Claim CTA. */
18
+ currentUserIsWinner?: boolean;
11
19
  labels?: {
12
20
  heading?: string;
13
21
  winnerLabel?: string;
@@ -18,4 +26,4 @@ export interface EventRaffleWinnerViewProps {
18
26
  anonymous?: string;
19
27
  };
20
28
  }
21
- export declare function EventRaffleWinnerView({ event, labels }: EventRaffleWinnerViewProps): import("react/jsx-runtime").JSX.Element;
29
+ export declare function EventRaffleWinnerView({ event, currentUserIsWinner, labels }: EventRaffleWinnerViewProps): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Article, Div, Heading, Span, Text, TextLink } from "../../../ui";
2
+ import { Article, ClaimCouponButton, Div, Heading, Span, Text, TextLink } from "../../../ui";
3
3
  const DEFAULT_LABELS = {
4
4
  heading: "Raffle Winner",
5
5
  winnerLabel: "Winner",
@@ -9,10 +9,10 @@ const DEFAULT_LABELS = {
9
9
  notDrawnYet: "The raffle has not been drawn yet.",
10
10
  anonymous: "Anonymous participant",
11
11
  };
12
- export function EventRaffleWinnerView({ event, labels }) {
12
+ export function EventRaffleWinnerView({ event, currentUserIsWinner, labels }) {
13
13
  const l = { ...DEFAULT_LABELS, ...(labels ?? {}) };
14
14
  if (!event.raffleWinnerUserId) {
15
15
  return (_jsx(Div, { className: "rounded-xl border border-dashed border-zinc-300 dark:border-zinc-700 px-6 py-10 text-center", children: _jsx(Text, { className: "text-zinc-500 dark:text-zinc-400", children: l.notDrawnYet }) }));
16
16
  }
17
- return (_jsxs(Article, { className: "space-y-4", children: [_jsxs(Heading, { level: 2, className: "text-xl font-semibold text-zinc-900 dark:text-zinc-100", children: ["\uD83C\uDF9F\uFE0F ", l.heading] }), _jsxs(Div, { className: "rounded-2xl border border-amber-200 bg-amber-50/60 dark:border-amber-700 dark:bg-amber-900/20 p-6 space-y-3", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-xs uppercase tracking-wide text-amber-700 dark:text-amber-300", children: l.winnerLabel }), _jsx(Text, { className: "mt-1 text-2xl font-bold text-amber-900 dark:text-amber-100", children: event.raffleWinnerDisplayName?.trim() || l.anonymous })] }), event.rafflePrize ? (_jsxs(Div, { children: [_jsx(Text, { className: "text-xs uppercase tracking-wide text-amber-700 dark:text-amber-300", children: l.prizeLabel }), _jsx(Text, { className: "mt-1 text-base text-zinc-800 dark:text-zinc-100", children: event.rafflePrize })] })) : null, _jsxs(Div, { className: "flex flex-wrap gap-x-6 gap-y-2 pt-2 text-sm text-zinc-600 dark:text-zinc-300", children: [typeof event.raffleEntryCount === "number" ? (_jsxs(Span, { children: [l.poolLabel, ":", " ", _jsx(Span, { className: "font-medium text-zinc-900 dark:text-zinc-100", children: event.raffleEntryCount })] })) : null, event.raffleTriggeredAt ? (_jsx(Span, { children: new Date(event.raffleTriggeredAt).toLocaleString() })) : null] })] }), event.raffleGithubFunctionUrl ? (_jsxs(Div, { className: "rounded-lg border border-zinc-200 dark:border-zinc-700 p-4 text-sm", children: [_jsx(Text, { className: "font-medium text-zinc-700 dark:text-zinc-200", children: l.fairnessLabel }), _jsx(TextLink, { href: event.raffleGithubFunctionUrl, className: "mt-1 block break-all text-primary-600 dark:text-primary-400 hover:underline", children: event.raffleGithubFunctionUrl })] })) : null] }));
17
+ return (_jsxs(Article, { className: "space-y-4", children: [_jsxs(Heading, { level: 2, className: "text-xl font-semibold text-zinc-900 dark:text-zinc-100", children: ["\uD83C\uDF9F\uFE0F ", l.heading] }), _jsxs(Div, { className: "rounded-2xl border border-amber-200 bg-amber-50/60 dark:border-amber-700 dark:bg-amber-900/20 p-6 space-y-3", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-xs uppercase tracking-wide text-amber-700 dark:text-amber-300", children: l.winnerLabel }), _jsx(Text, { className: "mt-1 text-2xl font-bold text-amber-900 dark:text-amber-100", children: event.raffleWinnerDisplayName?.trim() || l.anonymous })] }), event.rafflePrize ? (_jsxs(Div, { children: [_jsx(Text, { className: "text-xs uppercase tracking-wide text-amber-700 dark:text-amber-300", children: l.prizeLabel }), _jsx(Text, { className: "mt-1 text-base text-zinc-800 dark:text-zinc-100", children: event.rafflePrize })] })) : null, currentUserIsWinner && event.rafflePrizeCouponCode ? (_jsx(Div, { className: "pt-2", children: _jsx(ClaimCouponButton, { couponCode: event.rafflePrizeCouponCode, source: "raffle", size: "sm" }) })) : null, _jsxs(Div, { className: "flex flex-wrap gap-x-6 gap-y-2 pt-2 text-sm text-zinc-600 dark:text-zinc-300", children: [typeof event.raffleEntryCount === "number" ? (_jsxs(Span, { children: [l.poolLabel, ":", " ", _jsx(Span, { className: "font-medium text-zinc-900 dark:text-zinc-100", children: event.raffleEntryCount })] })) : null, event.raffleTriggeredAt ? (_jsx(Span, { children: new Date(event.raffleTriggeredAt).toLocaleString() })) : null] })] }), event.raffleGithubFunctionUrl ? (_jsxs(Div, { className: "rounded-lg border border-zinc-200 dark:border-zinc-700 p-4 text-sm", children: [_jsx(Text, { className: "font-medium text-zinc-700 dark:text-zinc-200", children: l.fairnessLabel }), _jsx(TextLink, { href: event.raffleGithubFunctionUrl, className: "mt-1 block break-all text-primary-600 dark:text-primary-400 hover:underline", children: event.raffleGithubFunctionUrl })] })) : null] }));
18
18
  }