@mohasinac/appkit 2.7.49 → 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 +16 -0
  9. package/dist/client.js +11 -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
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useCallback, useMemo, useState } from "react";
4
- import { Button, Div, Heading, LoginRequiredModal, Span, Text } from "../../../ui";
4
+ import { Button, ClaimCouponButton, Div, Heading, LoginRequiredModal, Span, Text } from "../../../ui";
5
5
  import { isAuthError } from "../../../utils/auth-error";
6
6
  const DEFAULT_LABELS = {
7
7
  heading: "Spin the Wheel",
@@ -67,5 +67,5 @@ export function SpinWheelView({ eventId, prizes, alreadyUsed, initialPrizeId, in
67
67
  0% { transform: rotate(0deg); }
68
68
  100% { transform: rotate(2160deg); }
69
69
  }
70
- ` })] }), wonPrize ? (_jsxs(Div, { className: "rounded-xl border border-emerald-300 bg-emerald-50 dark:border-emerald-700 dark:bg-emerald-900/30 p-4 text-center", children: [_jsx(Text, { className: "text-sm uppercase tracking-wide text-emerald-700 dark:text-emerald-300", children: l.wonHeadline }), _jsx(Text, { className: "mt-1 text-lg font-bold text-emerald-900 dark:text-emerald-100", children: wonPrize.label }), resultCoupon ? (_jsxs(Text, { className: "mt-2 text-sm text-emerald-800 dark:text-emerald-200", children: [l.couponHint, " ", _jsx(Span, { className: "font-mono font-semibold", children: resultCoupon })] })) : null] })) : null, error ? (_jsx(Text, { className: "text-sm text-rose-600 dark:text-rose-400 text-center", children: error })) : null, _jsx(Div, { className: "flex justify-center", children: _jsx(Button, { variant: "primary", size: "lg", onClick: handleSpin, disabled: disabled, children: spinning ? l.spinningButton : l.spinButton }) }), alreadyUsed ? (_jsx(Text, { className: "text-center text-sm text-zinc-500 dark:text-zinc-400", children: l.alreadySpun })) : !inWindow ? (_jsx(Text, { className: "text-center text-sm text-zinc-500 dark:text-zinc-400", children: l.outsideWindow })) : null] }));
70
+ ` })] }), wonPrize ? (_jsxs(Div, { className: "rounded-xl border border-emerald-300 bg-emerald-50 dark:border-emerald-700 dark:bg-emerald-900/30 p-4 text-center", children: [_jsx(Text, { className: "text-sm uppercase tracking-wide text-emerald-700 dark:text-emerald-300", children: l.wonHeadline }), _jsx(Text, { className: "mt-1 text-lg font-bold text-emerald-900 dark:text-emerald-100", children: wonPrize.label }), resultCoupon ? (_jsxs(_Fragment, { children: [_jsxs(Text, { className: "mt-2 text-sm text-emerald-800 dark:text-emerald-200", children: [l.couponHint, " ", _jsx(Span, { className: "font-mono font-semibold", children: resultCoupon })] }), _jsx(Div, { className: "mt-3 flex justify-center", children: _jsx(ClaimCouponButton, { couponCode: resultCoupon, size: "sm", source: "spin" }) })] })) : null] })) : null, error ? (_jsx(Text, { className: "text-sm text-rose-600 dark:text-rose-400 text-center", children: error })) : null, _jsx(Div, { className: "flex justify-center", children: _jsx(Button, { variant: "primary", size: "lg", onClick: handleSpin, disabled: disabled, children: spinning ? l.spinningButton : l.spinButton }) }), alreadyUsed ? (_jsx(Text, { className: "text-center text-sm text-zinc-500 dark:text-zinc-400", children: l.alreadySpun })) : !inWindow ? (_jsx(Text, { className: "text-center text-sm text-zinc-500 dark:text-zinc-400", children: l.outsideWindow })) : null] }));
71
71
  }
@@ -10,5 +10,10 @@ export interface BottomNavLayoutProps {
10
10
  *
11
11
  * Provides the `nav + ul` container with correct z-index, background, height,
12
12
  * and safe-area inset. Pass `li`-wrapped items as children.
13
+ *
14
+ * Mirrors `AppLayoutShell`'s `--header-height` contract: while mounted, writes
15
+ * `--bottom-nav-height: 4rem` to `:root` (sub-`lg` only — the nav itself is
16
+ * `lg:hidden`). Sticky CTAs read `var(--bottom-nav-height, 0px)` to position
17
+ * themselves above the navbar so they don't get clipped.
13
18
  */
14
19
  export declare function BottomNavLayout({ ariaLabel, children, id, className, }: BottomNavLayoutProps): import("react/jsx-runtime").JSX.Element;
@@ -1,11 +1,32 @@
1
+ "use client";
1
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect } from "react";
2
4
  import { Nav, Ul } from "../../ui";
3
5
  /**
4
6
  * BottomNavLayout — generic fixed-bottom navigation shell.
5
7
  *
6
8
  * Provides the `nav + ul` container with correct z-index, background, height,
7
9
  * and safe-area inset. Pass `li`-wrapped items as children.
10
+ *
11
+ * Mirrors `AppLayoutShell`'s `--header-height` contract: while mounted, writes
12
+ * `--bottom-nav-height: 4rem` to `:root` (sub-`lg` only — the nav itself is
13
+ * `lg:hidden`). Sticky CTAs read `var(--bottom-nav-height, 0px)` to position
14
+ * themselves above the navbar so they don't get clipped.
8
15
  */
9
16
  export function BottomNavLayout({ ariaLabel, children, id = "bottom-navbar", className, }) {
17
+ useEffect(() => {
18
+ const root = document.documentElement;
19
+ const mql = typeof window !== "undefined" ? window.matchMedia("(max-width: 1023.98px)") : null;
20
+ const sync = () => {
21
+ // Only expose the var when the navbar is actually rendered (below lg).
22
+ root.style.setProperty("--bottom-nav-height", mql && mql.matches ? "4rem" : "0px");
23
+ };
24
+ sync();
25
+ mql?.addEventListener("change", sync);
26
+ return () => {
27
+ mql?.removeEventListener("change", sync);
28
+ root.style.setProperty("--bottom-nav-height", "0px");
29
+ };
30
+ }, []);
10
31
  return (_jsx(Nav, { id: id, "aria-label": ariaLabel, className: `fixed bottom-0 left-0 right-0 lg:hidden z-40 bg-white/90 dark:bg-slate-950/90 backdrop-blur-md border-t border-zinc-200/80 dark:border-slate-800/80 shadow-2xl pb-safe${className ? ` ${className}` : ""}`, children: _jsx(Ul, { className: "flex items-stretch h-16", children: children }) }));
11
32
  }
@@ -4,6 +4,7 @@ import { useWishlistCount } from "../wishlist/hooks/useWishlistCount";
4
4
  import { useNotifications } from "../account/hooks/useNotifications";
5
5
  import { useDashboardNav } from "./DashboardNavContext";
6
6
  import { TitleBarLayout } from "./TitleBarLayout";
7
+ import { ROUTES } from "../../next/routing/route-map";
7
8
  /**
8
9
  * TitleBar � domain shell over TitleBarLayout.
9
10
  *
@@ -18,7 +19,12 @@ export function TitleBar({ suppressDashboardNav, onBeforeToggleDashboardNav, use
18
19
  const wishlistCount = useWishlistCount(userId);
19
20
  const { unreadCount } = useNotifications(1);
20
21
  const { hasNav: hasDashboardNav, toggleNav: toggleDashboardNav } = useDashboardNav();
21
- return (_jsx(TitleBarLayout, { ...rest, cartCount: cartCount, wishlistCount: wishlistCount, unreadNotificationCount: rest.user ? unreadCount : 0, hasDashboardNav: suppressDashboardNav ? false : hasDashboardNav, onToggleDashboardNav: suppressDashboardNav
22
+ return (_jsx(TitleBarLayout, { ...rest, cartCount: cartCount, wishlistCount: wishlistCount, unreadNotificationCount: rest.user ? unreadCount : 0,
23
+ // Default the bell to the user notifications page when the user is
24
+ // authenticated — consumers can override by passing their own
25
+ // `notificationsHref` through `...rest`.
26
+ notificationsHref: rest.notificationsHref ??
27
+ (rest.user ? String(ROUTES.USER.NOTIFICATIONS) : undefined), hasDashboardNav: suppressDashboardNav ? false : hasDashboardNav, onToggleDashboardNav: suppressDashboardNav
22
28
  ? undefined
23
29
  : () => {
24
30
  onBeforeToggleDashboardNav?.();
@@ -25,8 +25,14 @@ export interface TitleBarLayoutProps {
25
25
  cartHref?: string;
26
26
  cartCount?: number;
27
27
  profileHref?: string;
28
- /** Unread notification count to show as a badge on the profile icon. */
28
+ /** Unread notification count shown on the dedicated bell icon only. */
29
29
  unreadNotificationCount?: number;
30
+ /**
31
+ * Href for the dedicated notification bell icon rendered to the LEFT of the
32
+ * wishlist icon. When provided, the bell is rendered (with count badge) and
33
+ * navigates to the user notifications page by default.
34
+ */
35
+ notificationsHref?: string;
30
36
  loginHref?: string;
31
37
  registerHref?: string;
32
38
  user?: TitleBarUser | null;
@@ -58,4 +64,4 @@ export interface TitleBarLayoutProps {
58
64
  *
59
65
  * Receives all domain data as props — zero domain imports.
60
66
  */
61
- export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, unreadNotificationCount, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark, onToggleTheme, hasDashboardNav: _hasDashboardNav, onToggleDashboardNav: _onToggleDashboardNav, hideSidebarToggle, id, className, }: TitleBarLayoutProps): import("react/jsx-runtime").JSX.Element;
67
+ export declare function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount, cartHref, cartCount, profileHref, unreadNotificationCount, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark, onToggleTheme, hasDashboardNav: _hasDashboardNav, onToggleDashboardNav: _onToggleDashboardNav, hideSidebarToggle, id, className, }: TitleBarLayoutProps): import("react/jsx-runtime").JSX.Element;
@@ -14,7 +14,7 @@ const countBadge = "absolute -top-0.5 -right-0.5 flex items-center justify-cente
14
14
  *
15
15
  * Receives all domain data as props — zero domain imports.
16
16
  */
17
- export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, unreadNotificationCount = 0, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme, hasDashboardNav: _hasDashboardNav, onToggleDashboardNav: _onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
17
+ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, searchOpen: _searchOpen, brandName, brandShortName: _brandShortName, siteLogoUrl, logoHref, promotionsHref, compareHref, wishlistHref, wishlistCount = 0, cartHref, cartCount = 0, profileHref, unreadNotificationCount = 0, notificationsHref, loginHref, registerHref, user, notificationSlot, devSlot, navSlot, promoStripText, isDark = false, onToggleTheme, hasDashboardNav: _hasDashboardNav, onToggleDashboardNav: _onToggleDashboardNav, hideSidebarToggle = false, id = "titlebar", className = "", }) {
18
18
  // ── Element builders ────────────────────────────────────────────────────────
19
19
  const promotionsEl = promotionsHref ? (_jsxs(Link, { href: promotionsHref, "aria-label": "Today's deals", className: "flex items-center gap-1 px-3 py-1 rounded-full text-xs font-bold bg-primary-100 text-primary-700 dark:bg-secondary-900/40 dark:text-secondary-400 hover:bg-primary-200 dark:hover:bg-secondary-900/60 transition-colors border border-primary-200/60 dark:border-secondary-700/40", children: [_jsx("svg", { className: "w-3 h-3", viewBox: "0 0 24 24", fill: "currentColor", "aria-hidden": "true", children: _jsx("path", { d: "M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.37.86.58 1.41.58.55 0 1.05-.21 1.41-.59l7-7c.37-.36.59-.86.59-1.41 0-.55-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z" }) }), _jsx(Span, { className: "hidden sm:inline", children: "Today's Deals" })] })) : null;
20
20
  const themeBtn = onToggleTheme ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": isDark ? "Switch to light mode" : "Switch to dark mode", onClick: onToggleTheme, className: iconBtn, children: isDark ? (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 3v1m0 16v1m8.66-9h-1M4.34 12h-1m15.07-6.07-.71.71M6.34 17.66l-.71.71m12.73 0-.71-.71M6.34 6.34l-.71-.71M12 5a7 7 0 1 0 0 14A7 7 0 0 0 12 5z" }) })) : (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 12.79A9 9 0 1 1 11.21 3a7 7 0 1 0 9.79 9.79z" }) })) })) : null;
@@ -22,15 +22,19 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
22
22
  // Compare is always lg+ (desktop-only feature, less critical on mobile)
23
23
  const compareEl = compareHref ? (_jsx(Link, { href: compareHref, "aria-label": "Compare items", className: `${iconBtn} hidden lg:flex`, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" }) }) })) : null;
24
24
  const searchBtn = (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Search", onClick: onSearchToggle, className: iconBtn, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" }) }) }));
25
+ // Dedicated notification bell — rendered immediately before the wishlist
26
+ // icon (per layout request). Falls back to invisible when no href is set;
27
+ // the count badge mirrors the wishlist/cart pattern.
28
+ const notificationsEl = notificationsHref ? (_jsxs(Link, { href: notificationsHref, "aria-label": `Notifications${unreadNotificationCount > 0 ? `, ${unreadNotificationCount} unread` : ""}`, className: `relative ${iconBtn}`, children: [_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" }) }), unreadNotificationCount > 0 && (_jsx(Span, { className: countBadge, children: unreadNotificationCount > 99 ? "99+" : unreadNotificationCount }))] })) : null;
25
29
  const wishlistEl = wishlistHref ? (_jsxs(Link, { href: wishlistHref, "aria-label": `Wishlist${wishlistCount > 0 ? `, ${wishlistCount} items` : ""}`, className: `relative ${iconBtn}`, children: [_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4.318 6.318a4.5 4.5 0 0 0 0 6.364L12 20.364l7.682-7.682a4.5 4.5 0 0 0-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 0 0-6.364 0z" }) }), wishlistCount > 0 && (_jsx(Span, { className: countBadge, children: wishlistCount > 9 ? "9+" : wishlistCount }))] })) : null;
26
30
  const cartEl = cartHref ? (_jsxs(Link, { href: cartHref, "aria-label": `Cart${cartCount > 0 ? `, ${cartCount} items` : ""}`, className: `relative ${iconBtn}`, children: [_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0z" }) }), cartCount > 0 && (_jsx(Span, { className: countBadge, children: cartCount > 9 ? "9+" : cartCount }))] })) : null;
27
- const profileEl = profileHref ? (_jsxs(Link, { href: profileHref, "aria-label": user ? `Profile — ${user.displayName ?? user.email}${unreadNotificationCount > 0 ? `, ${unreadNotificationCount} unread alerts` : ""}` : "Sign in", className: `relative ${iconBtn}`, children: [user?.photoURL ? (
28
- // eslint-disable-next-line @next/next/no-img-element
29
- _jsx("img", { src: user.photoURL, alt: user.displayName ?? "Profile", width: 28, height: 28, loading: "lazy", className: "w-7 h-7 rounded-full object-cover" })) : (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0zM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632z" }) })), unreadNotificationCount > 0 && (_jsx(Span, { className: countBadge, children: unreadNotificationCount > 99 ? "99+" : unreadNotificationCount }))] })) : null;
31
+ const profileEl = profileHref ? (_jsx(Link, { href: profileHref, "aria-label": user ? `Profile — ${user.displayName ?? user.email}` : "Sign in", className: `relative ${iconBtn}`, children: user?.photoURL ? (
32
+ // eslint-disable-next-line @next/next/no-img-element
33
+ _jsx("img", { src: user.photoURL, alt: user.displayName ?? "Profile", width: 28, height: 28, loading: "lazy", className: "w-7 h-7 rounded-full object-cover" })) : (_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0zM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632z" }) })) })) : null;
30
34
  const authButtonsEl = !user && (loginHref || registerHref) ? (_jsxs(Row, { gap: "xs", className: "hidden lg:flex items-center", children: [loginHref && (_jsx(Link, { href: loginHref, className: "px-3 py-1.5 text-sm font-medium text-zinc-700 dark:text-zinc-300 hover:text-primary-700 dark:hover:text-secondary-400 transition-colors rounded-lg hover:bg-primary-50 dark:hover:bg-slate-800", children: "Sign in" })), registerHref && (_jsx(Link, { href: registerHref, className: "px-3 py-1.5 text-sm font-semibold rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors btn-glow shadow-sm", children: "Register" }))] })) : null;
31
- const hasTb2 = !!(wishlistEl || cartEl || profileEl);
35
+ const hasTb2 = !!(notificationsEl || wishlistEl || cartEl || profileEl);
32
36
  // ── Render ───────────────────────────────────────────────────────────────────
33
- return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-white/95 dark:bg-slate-950/95 backdrop-blur-md border-b border-zinc-100 dark:border-slate-800 shadow-sm ${className}`, children: [promoStripText && (_jsx(Div, { className: "bg-gradient-to-r from-primary-700 to-secondary-600 dark:from-primary-700 dark:to-cobalt-700 text-white text-xs py-1 text-center font-medium", children: promoStripText })), _jsxs(Div, { className: "container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", className: "relative h-14", children: [_jsx(Row, { gap: "3", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, className: "h-7 md:h-9 lg:h-10" }) }) }), siteLogoUrl ? (_jsx(Div, { className: "absolute inset-y-0 left-1/2 -translate-x-1/2 flex items-center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { src: siteLogoUrl, title: brandName, className: "h-7 md:h-9 lg:h-10" }) }) })) : (navSlot && _jsx(Div, { className: "hidden md:flex", children: navSlot })), _jsxs(Row, { gap: "xs", children: [devSlot, compareEl, notificationSlot, wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
37
+ return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-white/95 dark:bg-slate-950/95 backdrop-blur-md border-b border-zinc-100 dark:border-slate-800 shadow-sm ${className}`, children: [promoStripText && (_jsx(Div, { className: "bg-gradient-to-r from-primary-700 to-secondary-600 dark:from-primary-700 dark:to-cobalt-700 text-white text-xs py-1 text-center font-medium", children: promoStripText })), _jsxs(Div, { className: "container mx-auto px-4 sm:px-6 lg:px-8 max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", className: "relative h-14", children: [_jsx(Row, { gap: "3", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, className: "h-7 md:h-9 lg:h-10" }) }) }), siteLogoUrl ? (_jsx(Div, { className: "absolute inset-y-0 left-1/2 -translate-x-1/2 flex items-center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { src: siteLogoUrl, title: brandName, className: "h-7 md:h-9 lg:h-10" }) }) })) : (navSlot && _jsx(Div, { className: "hidden md:flex", children: navSlot })), _jsxs(Row, { gap: "xs", children: [devSlot, compareEl, notificationSlot, notificationsEl && _jsx(Div, { className: "hidden lg:flex", children: notificationsEl }), wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
34
38
  ? profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })
35
- : authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, promotionsEl, themeBtn, hamburgerBtn] })] }), hasTb2 && (_jsxs(Row, { as: "nav", "aria-label": "Account actions", justify: "end", gap: "xs", className: "flex lg:hidden h-10 border-t border-zinc-100 dark:border-slate-800 px-1", children: [wishlistEl, cartEl, profileEl] }))] })] }));
39
+ : authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, promotionsEl, themeBtn, hamburgerBtn] })] }), hasTb2 && (_jsxs(Row, { as: "nav", "aria-label": "Account actions", justify: "end", gap: "xs", className: "flex lg:hidden h-10 border-t border-zinc-100 dark:border-slate-800 px-1", children: [notificationsEl, wishlistEl, cartEl, profileEl] }))] })] }));
36
40
  }
@@ -143,9 +143,9 @@ export declare const orderSchema: z.ZodObject<{
143
143
  createdAt?: string | undefined;
144
144
  updatedAt?: string | undefined;
145
145
  notes?: string | undefined;
146
+ couponCode?: string | undefined;
146
147
  tax?: number | undefined;
147
148
  trackingNumber?: string | undefined;
148
- couponCode?: string | undefined;
149
149
  shippingCarrier?: string | undefined;
150
150
  discount?: number | undefined;
151
151
  paymentGateway?: string | undefined;
@@ -178,9 +178,9 @@ export declare const orderSchema: z.ZodObject<{
178
178
  createdAt?: string | undefined;
179
179
  updatedAt?: string | undefined;
180
180
  notes?: string | undefined;
181
+ couponCode?: string | undefined;
181
182
  tax?: number | undefined;
182
183
  trackingNumber?: string | undefined;
183
- couponCode?: string | undefined;
184
184
  shippingCarrier?: string | undefined;
185
185
  discount?: number | undefined;
186
186
  paymentGateway?: string | undefined;
@@ -116,12 +116,15 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
116
116
  isWishlisted: (productId) => wishlistedIds.has(productId),
117
117
  };
118
118
  const handleAddToCart = useCallback((product) => {
119
- localCart.add(product.id, 1, {
119
+ const snapshot = {
120
120
  productTitle: product.title,
121
121
  productImage: product.mainImage,
122
122
  price: product.price,
123
- });
124
- pushCartOp({ op: "add", productId: product.id, quantity: 1, productTitle: product.title, productImage: product.mainImage, price: product.price });
123
+ storeId: product.storeId,
124
+ storeName: product.storeName,
125
+ };
126
+ localCart.add(product.id, 1, snapshot);
127
+ pushCartOp({ op: "add", productId: product.id, quantity: 1, ...snapshot });
125
128
  showToast("Added to cart", "success");
126
129
  }, [localCart, showToast]);
127
130
  const selection = useBulkSelection({ items: preOrders, keyExtractor: (p) => p.id });
@@ -135,8 +138,9 @@ export function PreOrdersIndexListing({ initialData, categorySlug, brandName })
135
138
  onClick: () => {
136
139
  const selected = preOrders.filter((p) => selection.selectedIdSet.has(p.id));
137
140
  selected.forEach((p) => {
138
- localCart.add(p.id, 1, { productTitle: p.title, productImage: p.mainImage, price: p.price });
139
- pushCartOp({ op: "add", productId: p.id, quantity: 1, productTitle: p.title, productImage: p.mainImage, price: p.price });
141
+ const snapshot = { productTitle: p.title, productImage: p.mainImage, price: p.price, storeId: p.storeId, storeName: p.storeName };
142
+ localCart.add(p.id, 1, snapshot);
143
+ pushCartOp({ op: "add", productId: p.id, quantity: 1, ...snapshot });
140
144
  });
141
145
  showToast(`${selected.length} items added to cart`, "success");
142
146
  selection.clearSelection();
@@ -115,5 +115,5 @@ export async function PrizeDrawDetailPageView({ id, initialPrizeDraw, currentUse
115
115
  price: pricePerEntry,
116
116
  storeId: typeof p.storeId === "string" ? p.storeId : undefined,
117
117
  storeName: storeName ?? undefined,
118
- } }), _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: "hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRIZE_DRAWS), className: "hover:text-primary-600 transition-colors", children: "Prize Draws" }), _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(PreOrderDetailView, { renderGallery: () => (_jsx(PrizeDrawCollage, { items: items, hideWonState: true })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", className: "mb-2 flex-wrap", children: [_jsx(Span, { className: "inline-block rounded-full bg-fuchsia-100 dark:bg-fuchsia-900/30 px-2.5 py-0.5 text-xs font-semibold text-fuchsia-700 dark:text-fuchsia-300", children: "Prize Draw" }), _jsx(Span, { className: `inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold ${statusClass(revealStatus)}`, children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(Span, { className: "inline-block rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-0.5 text-xs font-medium text-zinc-700 dark:text-zinc-300", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { className: "inline-block rounded-full bg-fuchsia-100 dark:bg-fuchsia-900/30 px-2.5 py-0.5 text-xs font-semibold text-fuchsia-700 dark:text-fuchsia-300", children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "text-xl font-bold leading-snug text-zinc-900 dark:text-zinc-50 sm:text-2xl", children: title })] }), _jsx(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-4", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "Entries" }), _jsxs(Text, { className: "text-sm font-semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "Reveal window opens" }), _jsx(Text, { className: "text-sm font-medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "Reveal window closes" }), _jsx(Text, { className: "text-sm font-medium", children: revealEnd.toLocaleString() })] }))] }) }), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-sm leading-relaxed text-zinc-600 dark:text-zinc-400 line-clamp-4" })), safeSeller && (_jsx(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-3", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] uppercase tracking-wide text-zinc-400 dark:text-zinc-500 mb-0.5", children: "Sold by" }), _jsx(Text, { className: "text-sm font-semibold text-zinc-800 dark:text-zinc-200", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), 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 })), renderBuyBar: () => (_jsx(Div, { id: "prize-draw-buy-bar", className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5", children: _jsx(PrizeDrawEntryActions, { productId: String(product.id), productSlug: slug, title: title, thumb: thumb, pricePerEntry: pricePerEntry, currency: currency, remainingEntries: remaining, revealStatus: revealStatus, prizeGithubFileUrl: githubUrl }) })) }), _jsxs(BuyBar, { children: [_jsx(Span, { className: "mr-auto text-sm font-bold text-zinc-900 dark:text-zinc-50", children: formatCurrency(pricePerEntry, currency) }), _jsx("a", { href: "#prize-draw-buy-bar", className: "appkit-button appkit-button--primary appkit-button--sm flex-1", children: _jsx("span", { className: "appkit-button__content", children: ACTIONS.PRIZE_DRAW["enter-draw"].label }) })] })] })] }));
118
+ } }), _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: "hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRIZE_DRAWS), className: "hover:text-primary-600 transition-colors", children: "Prize Draws" }), _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(PreOrderDetailView, { renderGallery: () => (_jsx(PrizeDrawCollage, { items: items, hideWonState: true })), renderInfo: () => (_jsxs(Stack, { gap: "md", children: [_jsxs(Div, { children: [_jsxs(Row, { gap: "xs", className: "mb-2 flex-wrap", children: [_jsx(Span, { className: "inline-block rounded-full bg-fuchsia-100 dark:bg-fuchsia-900/30 px-2.5 py-0.5 text-xs font-semibold text-fuchsia-700 dark:text-fuchsia-300", children: "Prize Draw" }), _jsx(Span, { className: `inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold ${statusClass(revealStatus)}`, children: statusLabel(revealStatus) }), maxPerUser !== null && (_jsxs(Span, { className: "inline-block rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-0.5 text-xs font-medium text-zinc-700 dark:text-zinc-300", children: ["Limit: ", maxPerUser, " entries per customer"] })), maxPerUser !== null && userEntriesUsed !== null && (_jsxs(Span, { className: "inline-block rounded-full bg-fuchsia-100 dark:bg-fuchsia-900/30 px-2.5 py-0.5 text-xs font-semibold text-fuchsia-700 dark:text-fuchsia-300", children: ["You have used ", userEntriesUsed, "/", maxPerUser] }))] }), _jsx(Heading, { level: 1, className: "text-xl font-bold leading-snug text-zinc-900 dark:text-zinc-50 sm:text-2xl", children: title })] }), _jsx(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-4", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "Entries" }), _jsxs(Text, { className: "text-sm font-semibold", children: [current, " / ", max, " (", remaining, " left)"] })] }), revealStart && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "Reveal window opens" }), _jsx(Text, { className: "text-sm font-medium", children: revealStart.toLocaleString() })] })), revealEnd && (_jsxs(Row, { justify: "between", align: "center", children: [_jsx(Text, { className: "text-sm text-[var(--appkit-color-text-muted)]", children: "Reveal window closes" }), _jsx(Text, { className: "text-sm font-medium", children: revealEnd.toLocaleString() })] }))] }) }), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-sm leading-relaxed text-zinc-600 dark:text-zinc-400 line-clamp-4" })), safeSeller && (_jsx(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-3", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] uppercase tracking-wide text-zinc-400 dark:text-zinc-500 mb-0.5", children: "Sold by" }), _jsx(Text, { className: "text-sm font-semibold text-zinc-800 dark:text-zinc-200", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] })), 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 })), renderBuyBar: () => (_jsx(Div, { id: "prize-draw-buy-bar", className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5", children: _jsx(PrizeDrawEntryActions, { productId: String(product.id), productSlug: slug, title: title, thumb: thumb, pricePerEntry: pricePerEntry, currency: currency, remainingEntries: remaining, revealStatus: revealStatus, prizeGithubFileUrl: githubUrl, storeId: typeof p.storeId === "string" ? p.storeId : undefined, storeName: storeName ?? undefined }) })) }), _jsxs(BuyBar, { children: [_jsx(Span, { className: "mr-auto text-sm font-bold text-zinc-900 dark:text-zinc-50", children: formatCurrency(pricePerEntry, currency) }), _jsx("a", { href: "#prize-draw-buy-bar", className: "appkit-button appkit-button--primary appkit-button--sm flex-1", children: _jsx("span", { className: "appkit-button__content", children: ACTIONS.PRIZE_DRAW["enter-draw"].label }) })] })] })] }));
119
119
  }
@@ -8,6 +8,8 @@ export interface PrizeDrawEntryActionsProps {
8
8
  remainingEntries: number;
9
9
  revealStatus: "pending" | "open" | "closed" | undefined;
10
10
  prizeGithubFileUrl?: string;
11
+ storeId?: string;
12
+ storeName?: string;
11
13
  }
12
14
  /**
13
15
  * Client buy panel for a prize-draw detail page (SB4-G).
@@ -16,4 +18,4 @@ export interface PrizeDrawEntryActionsProps {
16
18
  * - On confirm → add 1 entry to guest cart and route to /user/cart
17
19
  * - "View RNG source" link → prizeGithubFileUrl
18
20
  */
19
- export declare function PrizeDrawEntryActions({ productId, productSlug, title, thumb, pricePerEntry, currency, remainingEntries, revealStatus, prizeGithubFileUrl, }: PrizeDrawEntryActionsProps): import("react/jsx-runtime").JSX.Element;
21
+ export declare function PrizeDrawEntryActions({ productId, productSlug, title, thumb, pricePerEntry, currency, remainingEntries, revealStatus, prizeGithubFileUrl, storeId, storeName, }: PrizeDrawEntryActionsProps): import("react/jsx-runtime").JSX.Element;
@@ -18,7 +18,7 @@ import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
18
18
  * - On confirm → add 1 entry to guest cart and route to /user/cart
19
19
  * - "View RNG source" link → prizeGithubFileUrl
20
20
  */
21
- export function PrizeDrawEntryActions({ productId, productSlug, title, thumb, pricePerEntry, currency, remainingEntries, revealStatus, prizeGithubFileUrl, }) {
21
+ export function PrizeDrawEntryActions({ productId, productSlug, title, thumb, pricePerEntry, currency, remainingEntries, revealStatus, prizeGithubFileUrl, storeId, storeName, }) {
22
22
  const router = useRouter();
23
23
  const cart = useGuestCart();
24
24
  const { showToast } = useToast();
@@ -31,22 +31,18 @@ export function PrizeDrawEntryActions({ productId, productSlug, title, thumb, pr
31
31
  requireAuth(ACTION_ID.ENTER_PRIZE_DRAW, () => setConsentOpen(true));
32
32
  }, [closed, requireAuth]);
33
33
  const handleConfirm = useCallback(() => {
34
- cart.add(productId, 1, {
34
+ const snapshot = {
35
35
  productTitle: title,
36
36
  productImage: thumb,
37
37
  price: pricePerEntry,
38
- });
39
- pushCartOp({
40
- op: "add",
41
- productId,
42
- quantity: 1,
43
- productTitle: title,
44
- productImage: thumb,
45
- price: pricePerEntry,
46
- });
38
+ storeId,
39
+ storeName,
40
+ };
41
+ cart.add(productId, 1, snapshot);
42
+ pushCartOp({ op: "add", productId, quantity: 1, ...snapshot });
47
43
  setConsentOpen(false);
48
44
  showToast("Entry added to cart", "success");
49
45
  router.push(String(ROUTES.USER.CART));
50
- }, [cart, productId, title, thumb, pricePerEntry, router, showToast]);
46
+ }, [cart, productId, title, thumb, pricePerEntry, storeId, storeName, router, showToast]);
51
47
  return (_jsxs(Stack, { gap: "md", children: [_jsxs(Text, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: [formatCurrency(pricePerEntry, currency), _jsx(Text, { as: "span", className: "ml-1 text-xs font-normal text-[var(--appkit-color-text-muted)]", children: "per entry" })] }), _jsx(Button, { action: closed ? undefined : ACTIONS.PRIZE_DRAW["enter-draw"], variant: "primary", size: "md", className: "w-full", disabled: closed, onClick: handleEnter, children: closed ? "Draw closed" : undefined }), prizeGithubFileUrl ? (_jsx("a", { href: prizeGithubFileUrl, target: "_blank", rel: "noopener noreferrer", className: "text-center text-xs font-medium text-primary-600 underline-offset-4 hover:underline dark:text-primary-400", children: "View RNG source code on GitHub \u2192" })) : null, _jsx(Text, { className: "text-center text-xs text-[var(--appkit-color-text-muted)]", children: "Winners chosen by Node.js crypto.randomInt \u2014 fully auditable. Entries are locked once paid; refunds only if the prize pool is exhausted." }), _jsx(NonRefundableConsentModal, { open: consentOpen, listingType: "prize-draw", itemTitle: title, priceLabel: formatCurrency(pricePerEntry, currency), onCancel: () => setConsentOpen(false), onConfirm: handleConfirm }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
52
48
  }
@@ -5,6 +5,10 @@ export interface ProductDetailActionsProps {
5
5
  productImage?: string;
6
6
  price?: number;
7
7
  currency?: string;
8
+ /** Store identifier (storeSlug) — propagated so guest cart lines carry it. */
9
+ storeId?: string;
10
+ /** Denormalised store display name — used by the cart group header. */
11
+ storeName?: string;
8
12
  inStock: boolean;
9
13
  variant?: "desktop" | "mobile";
10
14
  }
@@ -13,4 +17,4 @@ export interface ProductDetailActionsProps {
13
17
  * to the server APIs with guest-fallback. Replaces the previously-unwired
14
18
  * placeholder buttons inside ProductDetailPageView.
15
19
  */
16
- export declare function ProductDetailActions({ productId, productSlug, productTitle, productImage, price, inStock, variant, }: ProductDetailActionsProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function ProductDetailActions({ productId, productSlug, productTitle, productImage, price, storeId, storeName, inStock, variant, }: ProductDetailActionsProps): import("react/jsx-runtime").JSX.Element;
@@ -15,7 +15,7 @@ import { ACTION_ID } from "../constants/action-defs";
15
15
  * to the server APIs with guest-fallback. Replaces the previously-unwired
16
16
  * placeholder buttons inside ProductDetailPageView.
17
17
  */
18
- export function ProductDetailActions({ productId, productSlug, productTitle, productImage, price, inStock, variant = "desktop", }) {
18
+ export function ProductDetailActions({ productId, productSlug, productTitle, productImage, price, storeId, storeName, inStock, variant = "desktop", }) {
19
19
  const router = useRouter();
20
20
  const { showToast } = useToast();
21
21
  const { requireAuth, modalOpen, modalMessage, closeModal } = useAuthGate();
@@ -34,6 +34,8 @@ export function ProductDetailActions({ productId, productSlug, productTitle, pro
34
34
  productTitle,
35
35
  productImage,
36
36
  price,
37
+ storeId,
38
+ storeName,
37
39
  });
38
40
  then?.();
39
41
  }
@@ -30,6 +30,8 @@ export interface ProductDetailPageViewProps {
30
30
  productImage?: string;
31
31
  price: number | null;
32
32
  currency: string;
33
+ storeId?: string;
34
+ storeName?: string;
33
35
  inStock: boolean;
34
36
  variant: "desktop" | "mobile";
35
37
  }) => React.ReactNode;
@@ -270,6 +270,8 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
270
270
  productImage: product.mainImage,
271
271
  price,
272
272
  currency,
273
+ storeId: typeof p.storeId === "string" ? p.storeId : undefined,
274
+ storeName: typeof p.storeName === "string" ? p.storeName : undefined,
273
275
  inStock,
274
276
  variant: "desktop",
275
277
  })) : (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: inStock ? "Buy Now" : "Out of Stock" }), _jsx(Button, { variant: "secondary", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: inStock ? "Add to Cart" : "Out of Stock" }), _jsx(Button, { variant: "ghost", size: "md", className: "w-full", disabled: true, title: ACTION_NOT_WIRED, children: "\u2661 Add to Wishlist" })] })), allowOffers && productType === "simple" && price !== null && renderOfferAction?.({
@@ -298,6 +300,8 @@ export async function ProductDetailPageView({ slug, initialProduct, renderOfferA
298
300
  productImage: product.mainImage,
299
301
  price,
300
302
  currency,
303
+ storeId: typeof p.storeId === "string" ? p.storeId : undefined,
304
+ storeName: typeof p.storeName === "string" ? p.storeName : undefined,
301
305
  inStock,
302
306
  variant: "mobile",
303
307
  })) : (_jsxs(_Fragment, { children: [_jsx(Button, { variant: "secondary", size: "sm", className: "shrink-0", disabled: true, title: ACTION_NOT_WIRED, children: "Add to Cart" }), _jsx(Button, { variant: "primary", size: "sm", className: "flex-1", disabled: true, title: ACTION_NOT_WIRED, children: inStock ? "Buy Now" : "Out of Stock" })] }))] })] })] }));
@@ -73,15 +73,15 @@ export function ProductCard({ product, href, onClick, onAddToWishlist, isWishlis
73
73
  return (_jsx(Text, { className: `mt-1 text-[11px] font-medium ${low
74
74
  ? "text-amber-600 dark:text-amber-400"
75
75
  : "text-zinc-500 dark:text-zinc-400"}`, children: low ? `Only ${stock} left` : `${stock} in stock` }));
76
- })(), featuresList && product.features && product.features.length > 0 && (_jsx(FeatureBadgeList, { productFeatureIds: product.features, features: featuresList, maxVisible: PRODUCT_FEATURE_CARD_MAX_VISIBLE, className: "mt-2" })), (onAddToCart || onBuyNow) && (_jsxs(Div, { className: "mt-2 grid gap-1.5", style: { gridTemplateColumns: onBuyNow && onAddToCart ? "1fr 1fr" : "1fr" }, children: [onBuyNow && (_jsxs(Button, { type: "button", onClick: (e) => {
76
+ })(), featuresList && product.features && product.features.length > 0 && (_jsx(FeatureBadgeList, { productFeatureIds: product.features, features: featuresList, maxVisible: PRODUCT_FEATURE_CARD_MAX_VISIBLE, className: "mt-2" })), (onAddToCart || onBuyNow) && (_jsxs(Div, { className: "mt-2 grid gap-1.5", style: { gridTemplateColumns: onBuyNow && onAddToCart ? "1fr 1fr" : "1fr" }, children: [onBuyNow && (_jsxs(Button, { type: "button", variant: "primary", size: "sm", onClick: (e) => {
77
77
  e.stopPropagation();
78
78
  e.preventDefault();
79
79
  onBuyNow(product);
80
- }, className: "flex items-center justify-center gap-1 rounded-xl bg-primary py-2 text-xs font-semibold text-white hover:bg-primary/90 active:scale-[0.97] transition-all duration-150 btn-glow", children: [_jsx("svg", { className: "h-3.5 w-3.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2.5, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M13 10V3L4 14h7v7l9-11h-7z" }) }), "Buy Now"] })), onAddToCart && (_jsxs(Button, { type: "button", onClick: (e) => {
80
+ }, className: "flex items-center justify-center gap-1 text-xs btn-glow", children: [_jsx("svg", { className: "h-3.5 w-3.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2.5, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M13 10V3L4 14h7v7l9-11h-7z" }) }), "Buy Now"] })), onAddToCart && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: (e) => {
81
81
  e.stopPropagation();
82
82
  e.preventDefault();
83
83
  onAddToCart(product);
84
- }, className: "flex items-center justify-center gap-1 rounded-xl border-2 border-primary/40 bg-primary/5 py-2 text-xs font-semibold text-zinc-800 hover:bg-primary/10 hover:border-primary/60 active:scale-[0.97] transition-all duration-150 dark:text-zinc-100 dark:border-primary-400/50 dark:bg-primary/[0.08] dark:hover:bg-primary/[0.15] dark:hover:border-primary-400/70", children: [_jsx("svg", { className: "h-3.5 w-3.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 3h2l.4 2M7 13h10l4-8H5.4m1.6 8L5 3H3m4 10v7a1 1 0 001 1h8a1 1 0 001-1v-7M9 21h6" }) }), "Cart"] }))] }))] })] })] }));
84
+ }, className: "flex items-center justify-center gap-1 text-xs", children: [_jsx("svg", { className: "h-3.5 w-3.5 flex-shrink-0", fill: "none", stroke: "currentColor", strokeWidth: 2, viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M3 3h2l.4 2M7 13h10l4-8H5.4m1.6 8L5 3H3m4 10v7a1 1 0 001 1h8a1 1 0 001-1v-7M9 21h6" }) }), "Cart"] }))] }))] })] })] }));
85
85
  if (href && !selectionMode) {
86
86
  return (_jsx(Link, { href: href, className: "block h-full", children: cardBody }));
87
87
  }
@@ -107,22 +107,28 @@ export function ProductsIndexListing({ initialData }) {
107
107
  });
108
108
  }, [wishlistedIds, localWishlist, showToast, requireAuth]);
109
109
  const handleAddToCart = useCallback((product) => {
110
- localCart.add(product.id, 1, {
110
+ const snapshot = {
111
111
  productTitle: product.title,
112
112
  productImage: product.mainImage,
113
113
  price: product.price,
114
- });
115
- pushCartOp({ op: "add", productId: product.id, quantity: 1, productTitle: product.title, productImage: product.mainImage, price: product.price });
114
+ storeId: product.storeId,
115
+ storeName: product.storeName,
116
+ };
117
+ localCart.add(product.id, 1, snapshot);
118
+ pushCartOp({ op: "add", productId: product.id, quantity: 1, ...snapshot });
116
119
  showToast("Added to cart", "success");
117
120
  }, [localCart, showToast]);
118
121
  const handleBuyNow = useCallback((product) => {
119
122
  requireAuth(ACTION_ID.BUY_NOW, () => {
120
- localCart.add(product.id, 1, {
123
+ const snapshot = {
121
124
  productTitle: product.title,
122
125
  productImage: product.mainImage,
123
126
  price: product.price,
124
- });
125
- pushCartOp({ op: "add", productId: product.id, quantity: 1, productTitle: product.title, productImage: product.mainImage, price: product.price });
127
+ storeId: product.storeId,
128
+ storeName: product.storeName,
129
+ };
130
+ localCart.add(product.id, 1, snapshot);
131
+ pushCartOp({ op: "add", productId: product.id, quantity: 1, ...snapshot });
126
132
  router.push(String(ROUTES.USER.CART));
127
133
  });
128
134
  }, [localCart, router, requireAuth]);
@@ -135,8 +141,9 @@ export function ProductsIndexListing({ initialData }) {
135
141
  onClick: () => {
136
142
  const selected = products.filter((p) => selection.selectedIdSet.has(p.id));
137
143
  selected.forEach((p) => {
138
- localCart.add(p.id, 1, { productTitle: p.title, productImage: p.mainImage, price: p.price });
139
- pushCartOp({ op: "add", productId: p.id, quantity: 1, productTitle: p.title, productImage: p.mainImage, price: p.price });
144
+ const snapshot = { productTitle: p.title, productImage: p.mainImage, price: p.price, storeId: p.storeId, storeName: p.storeName };
145
+ localCart.add(p.id, 1, snapshot);
146
+ pushCartOp({ op: "add", productId: p.id, quantity: 1, ...snapshot });
140
147
  });
141
148
  showToast(`${selected.length} items added to cart`, "success");
142
149
  selection.clearSelection();
@@ -2,6 +2,9 @@ import type { CouponItem } from "../types";
2
2
  export interface CouponCardLabels {
3
3
  copy?: string;
4
4
  copied?: string;
5
+ claim?: string;
6
+ claiming?: string;
7
+ claimed?: string;
5
8
  expires?: string;
6
9
  minOrder?: string;
7
10
  off?: string;
@@ -29,6 +32,13 @@ interface CouponCardProps {
29
32
  onEdit?: (id: string) => void;
30
33
  onToggleActive?: (id: string, currentlyActive: boolean) => void | Promise<void>;
31
34
  onDelete?: (id: string) => void | Promise<void>;
35
+ /**
36
+ * Hide the public Claim CTA. Defaults to false. Set true on admin/CRUD
37
+ * surfaces where the user is managing coupons, not redeeming them.
38
+ */
39
+ hideClaim?: boolean;
40
+ /** Source enum forwarded to /api/user/coupons/claim for analytics. */
41
+ claimSource?: "manual" | "promo" | "spin" | "raffle" | "prize-draw";
32
42
  }
33
- export declare function CouponCard({ coupon, labels: labelsProp, onCopy, className, selectable, isSelected, onSelect, onEdit, onToggleActive, onDelete, }: CouponCardProps): import("react/jsx-runtime").JSX.Element;
43
+ export declare function CouponCard({ coupon, labels: labelsProp, onCopy, className, selectable, isSelected, onSelect, onEdit, onToggleActive, onDelete, hideClaim, claimSource, }: CouponCardProps): import("react/jsx-runtime").JSX.Element;
34
44
  export {};
@@ -68,6 +68,9 @@ function formatDiscount(n, labels) {
68
68
  const DEFAULT_LABELS = {
69
69
  copy: "Copy",
70
70
  copied: "Copied!",
71
+ claim: "Claim",
72
+ claiming: "Claiming…",
73
+ claimed: "Use →",
71
74
  expires: "Expires",
72
75
  minOrder: "Min order",
73
76
  off: "OFF",
@@ -81,16 +84,20 @@ const DEFAULT_LABELS = {
81
84
  delete: "Delete",
82
85
  deleteConfirm: "Delete this coupon? This cannot be undone.",
83
86
  };
84
- export function CouponCard({ coupon, labels: labelsProp, onCopy, className = "", selectable = false, isSelected = false, onSelect, onEdit, onToggleActive, onDelete, }) {
87
+ export function CouponCard({ coupon, labels: labelsProp, onCopy, className = "", selectable = false, isSelected = false, onSelect, onEdit, onToggleActive, onDelete, hideClaim, claimSource = "manual", }) {
85
88
  const labels = { ...DEFAULT_LABELS, ...labelsProp };
86
89
  const n = normalize(coupon);
87
90
  const colors = TYPE_COLORS[n.type] ?? TYPE_COLORS.percentage;
88
91
  const [copied, setCopied] = useState(false);
92
+ const [claiming, setClaiming] = useState(false);
89
93
  const [busy, setBusy] = useState(null);
90
94
  const longPress = useLongPress(() => onSelect?.(n.id, !isSelected));
91
95
  const expiry = formatDateSafe(n.expiresAt);
92
96
  const discountLabel = formatDiscount(n, labels);
93
97
  const hasAdminActions = Boolean(onEdit || onToggleActive || onDelete);
98
+ // Show Claim by default on public surfaces; hide automatically when this
99
+ // card is being rendered with admin/CRUD actions wired.
100
+ const showClaim = hideClaim === undefined ? !hasAdminActions : !hideClaim;
94
101
  const handleCopy = () => {
95
102
  if (!n.code)
96
103
  return;
@@ -99,6 +106,33 @@ export function CouponCard({ coupon, labels: labelsProp, onCopy, className = "",
99
106
  onCopy?.(n.code);
100
107
  setTimeout(() => setCopied(false), 2000);
101
108
  };
109
+ const handleClaim = async () => {
110
+ if (!n.code || claiming)
111
+ return;
112
+ setClaiming(true);
113
+ try {
114
+ // Best-effort: claim into the wallet (anon users get 401, which we
115
+ // ignore so the deep-link still works), then jump to cart with the
116
+ // code pre-filled.
117
+ try {
118
+ await fetch("/api/user/coupons/claim", {
119
+ method: "POST",
120
+ headers: { "Content-Type": "application/json" },
121
+ credentials: "include",
122
+ body: JSON.stringify({ couponCode: n.code, source: claimSource }),
123
+ });
124
+ }
125
+ catch {
126
+ /* anon/network — non-fatal */
127
+ }
128
+ if (typeof window !== "undefined") {
129
+ window.location.href = `/cart?coupon=${encodeURIComponent(n.code)}`;
130
+ }
131
+ }
132
+ finally {
133
+ setClaiming(false);
134
+ }
135
+ };
102
136
  const handleToggle = async () => {
103
137
  if (!onToggleActive)
104
138
  return;
@@ -125,7 +159,7 @@ export function CouponCard({ coupon, labels: labelsProp, onCopy, className = "",
125
159
  };
126
160
  return (_jsxs(Div, { className: `group relative flex h-full flex-col rounded-xl border-2 p-4 ${colors.card} ${isSelected ? "ring-2 ring-primary" : ""} ${!n.isActive ? "opacity-70" : ""} ${className}`, onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(n.id, !isSelected); }, label: isSelected ? "Deselect coupon" : "Select coupon", position: "top-2 right-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100 transition-opacity" })), (hasAdminActions || n.scope) && (_jsxs(Row, { gap: "xs", className: "mb-2 flex-wrap", children: [_jsx(Span, { className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide ${n.isActive
127
161
  ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300"
128
- : "bg-zinc-200 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300"}`, children: n.isActive ? labels.active : labels.inactive }), n.scope && (_jsx(Span, { className: "inline-flex items-center rounded-full bg-zinc-200/70 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-zinc-700 dark:bg-zinc-700/60 dark:text-zinc-200", children: n.scope }))] })), _jsxs(Div, { className: "mb-3", children: [_jsx(Text, { className: "text-2xl font-extrabold tracking-tight leading-none", children: discountLabel }), n.name && (_jsx(Text, { className: "text-sm mt-0.5 font-medium opacity-80", children: n.name }))] }), n.description && (_jsx(Text, { className: "text-xs opacity-60 mb-3", children: n.description })), _jsxs("div", { className: `flex items-center gap-2 rounded-lg border border-dashed px-3 py-2 ${colors.code}`, children: [_jsx("span", { className: "flex-1 font-mono text-sm font-bold tracking-widest uppercase select-all", children: n.code || "—" }), n.code && (_jsx("button", { type: "button", onClick: handleCopy, className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-semibold transition-all hover:opacity-80 active:scale-95 bg-white/60 dark:bg-black/20 border border-current/20", "aria-label": "Copy coupon code", children: copied ? labels.copied : labels.copy }))] }), _jsxs(Row, { wrap: true, gap: "sm", className: "text-xs opacity-60 mt-2", children: [n.minOrderAmount ? (_jsxs(Span, { children: [labels.minOrder, ": \u20B9", (n.minOrderAmount / 100).toFixed(0)] })) : null, expiry && (_jsxs(Span, { children: [labels.expires, ": ", expiry] })), n.usageLimit ? (_jsxs(Span, { children: [n.usageCount, "/", n.usageLimit, " ", labels.used] })) : n.usageCount > 0 ? (_jsxs(Span, { children: [n.usageCount, " ", labels.used] })) : null] }), hasAdminActions && (_jsxs(Row, { gap: "xs", className: "mt-auto pt-3 justify-end", children: [onEdit && (_jsx("button", { type: "button", onClick: () => onEdit(n.id), title: labels.edit, "aria-label": labels.edit, className: "rounded-lg p-1.5 text-zinc-500 hover:bg-white/60 dark:hover:bg-black/30 hover:text-zinc-800 dark:hover:text-zinc-200 transition-colors", children: _jsx(Pencil, { className: "h-4 w-4" }) })), onToggleActive && (_jsx("button", { type: "button", onClick: handleToggle, disabled: busy === "toggle", title: n.isActive ? labels.deactivate : labels.activate, "aria-label": n.isActive ? labels.deactivate : labels.activate, className: "rounded-lg p-1.5 text-zinc-500 hover:bg-white/60 dark:hover:bg-black/30 transition-colors disabled:opacity-50", children: n.isActive
162
+ : "bg-zinc-200 text-zinc-600 dark:bg-zinc-700 dark:text-zinc-300"}`, children: n.isActive ? labels.active : labels.inactive }), n.scope && (_jsx(Span, { className: "inline-flex items-center rounded-full bg-zinc-200/70 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-zinc-700 dark:bg-zinc-700/60 dark:text-zinc-200", children: n.scope }))] })), _jsxs(Div, { className: "mb-3", children: [_jsx(Text, { className: "text-2xl font-extrabold tracking-tight leading-none", children: discountLabel }), n.name && (_jsx(Text, { className: "text-sm mt-0.5 font-medium opacity-80", children: n.name }))] }), n.description && (_jsx(Text, { className: "text-xs opacity-60 mb-3", children: n.description })), _jsxs("div", { className: `flex items-center gap-2 rounded-lg border border-dashed px-3 py-2 ${colors.code}`, children: [_jsx("span", { className: "flex-1 font-mono text-sm font-bold tracking-widest uppercase select-all", children: n.code || "—" }), n.code && (_jsx("button", { type: "button", onClick: handleCopy, className: "shrink-0 rounded-md px-2.5 py-1 text-xs font-semibold transition-all hover:opacity-80 active:scale-95 bg-white/60 dark:bg-black/20 border border-current/20", "aria-label": "Copy coupon code", children: copied ? labels.copied : labels.copy }))] }), showClaim && n.code && n.isActive && (_jsx("div", { className: "mt-2", children: _jsx("button", { type: "button", onClick: handleClaim, disabled: claiming, className: "w-full rounded-md px-3 py-1.5 text-xs font-semibold transition-all active:scale-95 bg-[var(--appkit-color-primary-700)] text-[var(--appkit-color-text-on-primary,#fff)] hover:bg-[var(--appkit-color-primary-800)] disabled:opacity-50", "aria-label": `Claim coupon ${n.code} and apply at checkout`, children: claiming ? labels.claiming : labels.claim }) })), _jsxs(Row, { wrap: true, gap: "sm", className: "text-xs opacity-60 mt-2", children: [n.minOrderAmount ? (_jsxs(Span, { children: [labels.minOrder, ": \u20B9", (n.minOrderAmount / 100).toFixed(0)] })) : null, expiry && (_jsxs(Span, { children: [labels.expires, ": ", expiry] })), n.usageLimit ? (_jsxs(Span, { children: [n.usageCount, "/", n.usageLimit, " ", labels.used] })) : n.usageCount > 0 ? (_jsxs(Span, { children: [n.usageCount, " ", labels.used] })) : null] }), hasAdminActions && (_jsxs(Row, { gap: "xs", className: "mt-auto pt-3 justify-end", children: [onEdit && (_jsx("button", { type: "button", onClick: () => onEdit(n.id), title: labels.edit, "aria-label": labels.edit, className: "rounded-lg p-1.5 text-zinc-500 hover:bg-white/60 dark:hover:bg-black/30 hover:text-zinc-800 dark:hover:text-zinc-200 transition-colors", children: _jsx(Pencil, { className: "h-4 w-4" }) })), onToggleActive && (_jsx("button", { type: "button", onClick: handleToggle, disabled: busy === "toggle", title: n.isActive ? labels.deactivate : labels.activate, "aria-label": n.isActive ? labels.deactivate : labels.activate, className: "rounded-lg p-1.5 text-zinc-500 hover:bg-white/60 dark:hover:bg-black/30 transition-colors disabled:opacity-50", children: n.isActive
129
163
  ? _jsx(ToggleRight, { className: "h-4 w-4 text-emerald-600 dark:text-emerald-400" })
130
164
  : _jsx(ToggleLeft, { className: "h-4 w-4" }) })), onDelete && (_jsx("button", { type: "button", onClick: handleDelete, disabled: busy === "delete", title: labels.delete, "aria-label": labels.delete, className: "rounded-lg p-1.5 text-zinc-500 hover:bg-red-50 dark:hover:bg-red-950/40 hover:text-red-600 dark:hover:text-red-400 transition-colors disabled:opacity-50", children: _jsx(Trash2, { className: "h-4 w-4" }) }))] }))] }));
131
165
  }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Claimed Coupons Repository — plan §10
3
+ *
4
+ * One row per (userId, couponCode). Top-level collection so the user wallet
5
+ * page is a single indexed read. Status is lazily flipped to "expired" on
6
+ * read when `expiresAt < now`. Soft-delete only (status stays, no destroy).
7
+ */
8
+ import { BaseRepository } from "../../../providers/db-firebase";
9
+ import { type ClaimedCouponDocument, type ClaimedCouponSource, type ClaimedCouponSnapshot } from "../schemas";
10
+ export interface ClaimedCouponCreateInput {
11
+ userId: string;
12
+ couponId: string;
13
+ couponCode: string;
14
+ source: ClaimedCouponSource;
15
+ couponSnapshot: ClaimedCouponSnapshot;
16
+ expiresAt?: Date | null;
17
+ }
18
+ export declare class ClaimedCouponsRepository extends BaseRepository<ClaimedCouponDocument> {
19
+ constructor();
20
+ findByUserAndCode(userId: string, couponCode: string): Promise<ClaimedCouponDocument | null>;
21
+ /** Idempotent claim — returns the existing claim if one already exists. */
22
+ claim(input: ClaimedCouponCreateInput): Promise<ClaimedCouponDocument>;
23
+ listForUser(userId: string): Promise<ClaimedCouponDocument[]>;
24
+ markUsed(userId: string, couponCode: string, orderId: string): Promise<void>;
25
+ softRemove(userId: string, couponCode: string): Promise<void>;
26
+ }
27
+ export declare const claimedCouponsRepository: ClaimedCouponsRepository;