@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.
- package/dist/_internal/client/features/layout/SidebarCollapseToggle.d.ts +19 -0
- package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +15 -0
- package/dist/_internal/client/features/layout/index.d.ts +2 -0
- package/dist/_internal/client/features/layout/index.js +1 -0
- package/dist/_internal/server/features/checkout/actions.js +25 -2
- package/dist/_internal/server/features/promotions/actions.js +3 -1
- package/dist/_internal/shared/actions/action-registry.js +149 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +11 -0
- package/dist/features/account/components/UserSidebar.js +2 -1
- package/dist/features/admin/components/AdminBlogEditorView.js +6 -2
- package/dist/features/admin/components/AdminSidebar.js +2 -1
- package/dist/features/auth/repository/session.repository.js +16 -1
- package/dist/features/cart/hooks/useAddToCart.d.ts +2 -0
- package/dist/features/cart/hooks/useAddToCart.js +2 -0
- package/dist/features/cart/hooks/useGuestCart.d.ts +2 -0
- package/dist/features/cart/utils/guest-cart.d.ts +7 -0
- package/dist/features/cart/utils/pending-ops.d.ts +6 -0
- package/dist/features/categories/components/CategoryBundlesListing.js +9 -1
- package/dist/features/categories/components/CategoryProductsListing.js +6 -10
- package/dist/features/categories/schemas/firestore.d.ts +9 -0
- package/dist/features/events/components/EventRaffleWinnerView.d.ts +9 -1
- package/dist/features/events/components/EventRaffleWinnerView.js +3 -3
- package/dist/features/events/components/SpinWheelView.js +3 -3
- package/dist/features/layout/BottomNavLayout.d.ts +5 -0
- package/dist/features/layout/BottomNavLayout.js +21 -0
- package/dist/features/layout/TitleBar.js +7 -1
- package/dist/features/layout/TitleBarLayout.d.ts +8 -2
- package/dist/features/layout/TitleBarLayout.js +11 -7
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +9 -5
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +3 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +8 -12
- package/dist/features/products/components/ProductDetailActions.d.ts +5 -1
- package/dist/features/products/components/ProductDetailActions.js +3 -1
- package/dist/features/products/components/ProductDetailPageView.d.ts +2 -0
- package/dist/features/products/components/ProductDetailPageView.js +4 -0
- package/dist/features/products/components/ProductGrid.js +3 -3
- package/dist/features/products/components/ProductsIndexListing.js +15 -8
- package/dist/features/promotions/components/CouponCard.d.ts +11 -1
- package/dist/features/promotions/components/CouponCard.js +36 -2
- package/dist/features/promotions/repository/claimed-coupons.repository.d.ts +27 -0
- package/dist/features/promotions/repository/claimed-coupons.repository.js +115 -0
- package/dist/features/promotions/schemas/firestore.d.ts +34 -0
- package/dist/features/promotions/schemas/firestore.js +13 -0
- package/dist/features/seller/components/SellerAddressesView.js +3 -2
- package/dist/features/seller/components/SellerBidsView.js +33 -3
- package/dist/features/seller/components/SellerOrdersView.js +35 -0
- package/dist/features/seller/components/SellerProductShell.d.ts +2 -0
- package/dist/features/seller/components/SellerProductShell.js +26 -4
- package/dist/features/seller/components/SellerProductsCards.d.ts +19 -0
- package/dist/features/seller/components/SellerProductsCards.js +19 -0
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +14 -0
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +20 -0
- package/dist/features/seller/components/SellerProductsView.d.ts +3 -1
- package/dist/features/seller/components/SellerProductsView.js +68 -19
- package/dist/features/seller/components/SellerReviewsView.js +67 -4
- package/dist/features/seller/components/SellerSidebar.js +2 -1
- package/dist/features/seller/components/seller-products-styles.d.ts +7 -0
- package/dist/features/seller/components/seller-products-styles.js +14 -0
- package/dist/features/shell/FormShell.d.ts +7 -1
- package/dist/features/shell/FormShell.js +5 -2
- package/dist/features/store-extensions/index.d.ts +4 -0
- package/dist/features/store-extensions/index.js +4 -0
- package/dist/features/store-extensions/repository/rbac.repositories.d.ts +23 -0
- package/dist/features/store-extensions/repository/rbac.repositories.js +32 -0
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +91 -0
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +127 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +244 -0
- package/dist/features/store-extensions/schemas/firestore.js +158 -0
- package/dist/features/store-extensions/schemas/rbac.d.ts +65 -0
- package/dist/features/store-extensions/schemas/rbac.js +43 -0
- package/dist/features/stores/components/InteractiveStoreCard.js +7 -3
- package/dist/features/stores/components/StoreProductsListing.js +6 -10
- package/dist/features/stores/schemas/firestore.d.ts +2 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +8 -0
- package/dist/next/routing/route-map.d.ts +79 -0
- package/dist/next/routing/route-map.js +34 -0
- package/dist/react/hooks/useFormStatePreservation.d.ts +17 -0
- package/dist/react/hooks/useFormStatePreservation.js +62 -0
- package/dist/react/hooks/useInlineRowEdit.d.ts +24 -0
- package/dist/react/hooks/useInlineRowEdit.js +68 -0
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.js +4 -0
- package/dist/repositories/index.d.ts +4 -0
- package/dist/repositories/index.js +5 -0
- package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
- package/dist/seed/claimed-coupons-seed-data.d.ts +13 -0
- package/dist/seed/claimed-coupons-seed-data.js +79 -0
- package/dist/seed/index.d.ts +2 -0
- package/dist/seed/index.js +3 -0
- package/dist/seed/manifest.js +17 -0
- package/dist/seed/store-extensions-seed-data.d.ts +19 -0
- package/dist/seed/store-extensions-seed-data.js +421 -0
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.style.css +20 -12
- package/dist/ui/components/ClaimCouponButton.d.ts +28 -0
- package/dist/ui/components/ClaimCouponButton.js +89 -0
- package/dist/ui/components/QuickCreateModal.d.ts +15 -0
- package/dist/ui/components/QuickCreateModal.js +48 -0
- package/dist/ui/components/QuickCreateModal.style.css +84 -0
- package/dist/ui/components/StickyBottomBar.d.ts +22 -0
- package/dist/ui/components/StickyBottomBar.js +20 -0
- package/dist/ui/components/VacationBanner.d.ts +11 -0
- package/dist/ui/components/VacationBanner.js +16 -0
- package/dist/ui/index.d.ts +8 -0
- package/dist/ui/index.js +4 -0
- 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,
|
|
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
|
|
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 ? (
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
119
|
+
const snapshot = {
|
|
120
120
|
productTitle: product.title,
|
|
121
121
|
productImage: product.mainImage,
|
|
122
122
|
price: product.price,
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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
|
-
|
|
34
|
+
const snapshot = {
|
|
35
35
|
productTitle: title,
|
|
36
36
|
productImage: thumb,
|
|
37
37
|
price: pricePerEntry,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
-
|
|
110
|
+
const snapshot = {
|
|
111
111
|
productTitle: product.title,
|
|
112
112
|
productImage: product.mainImage,
|
|
113
113
|
price: product.price,
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
123
|
+
const snapshot = {
|
|
121
124
|
productTitle: product.title,
|
|
122
125
|
productImage: product.mainImage,
|
|
123
126
|
price: product.price,
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
139
|
-
|
|
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;
|