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