@mohasinac/appkit 2.7.49 → 2.7.52
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/server/jobs/core/__tests__/couponExpiry.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/couponExpiry.test.js +65 -0
- package/dist/_internal/server/jobs/core/__tests__/listingProcessor.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/listingProcessor.test.js +163 -0
- package/dist/_internal/server/jobs/core/__tests__/mediaTmpCleanup.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/mediaTmpCleanup.test.js +115 -0
- package/dist/_internal/server/jobs/core/__tests__/pendingOrderTimeout.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/pendingOrderTimeout.test.js +96 -0
- package/dist/_internal/shared/actions/action-registry.js +149 -0
- package/dist/client.d.ts +45 -6
- package/dist/client.js +27 -4
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/features/_guide-cls.d.ts +13 -0
- package/dist/features/_guide-cls.js +14 -0
- package/dist/features/about/components/HelpPageView.js +29 -26
- package/dist/features/account/components/BuyerAccountGuideView.d.ts +1 -0
- package/dist/features/account/components/BuyerAccountGuideView.js +39 -0
- package/dist/features/account/components/BuyerAuctionsGuideView.d.ts +1 -0
- package/dist/features/account/components/BuyerAuctionsGuideView.js +38 -0
- package/dist/features/account/components/BuyerOrdersGuideView.d.ts +1 -0
- package/dist/features/account/components/BuyerOrdersGuideView.js +48 -0
- package/dist/features/account/components/BuyerShoppingGuideView.d.ts +1 -0
- package/dist/features/account/components/BuyerShoppingGuideView.js +38 -0
- package/dist/features/account/components/UserSidebar.js +2 -1
- package/dist/features/admin/components/AdminAnalyticsGuideView.d.ts +1 -0
- package/dist/features/admin/components/AdminAnalyticsGuideView.js +24 -0
- package/dist/features/admin/components/AdminBlogEditorView.js +102 -75
- package/dist/features/admin/components/AdminBundleEditorView.js +20 -14
- package/dist/features/admin/components/AdminBundlesView.d.ts +1 -3
- package/dist/features/admin/components/AdminBundlesView.js +173 -52
- package/dist/features/admin/components/AdminCatalogGuideView.d.ts +1 -0
- package/dist/features/admin/components/AdminCatalogGuideView.js +28 -0
- package/dist/features/admin/components/AdminContentGuideView.d.ts +1 -0
- package/dist/features/admin/components/AdminContentGuideView.js +36 -0
- package/dist/features/admin/components/AdminCouponEditorView.js +1 -1
- package/dist/features/admin/components/AdminGuideHubView.d.ts +5 -0
- package/dist/features/admin/components/AdminGuideHubView.js +79 -0
- package/dist/features/admin/components/AdminOrdersGuideView.d.ts +1 -0
- package/dist/features/admin/components/AdminOrdersGuideView.js +44 -0
- package/dist/features/admin/components/AdminProductsView.js +8 -3
- package/dist/features/admin/components/AdminSidebar.js +2 -1
- package/dist/features/admin/components/AdminSiteConfigGuideView.d.ts +1 -0
- package/dist/features/admin/components/AdminSiteConfigGuideView.js +21 -0
- package/dist/features/admin/components/AdminStoresGuideView.d.ts +1 -0
- package/dist/features/admin/components/AdminStoresGuideView.js +32 -0
- package/dist/features/admin/components/AdminTeamGuideView.d.ts +1 -0
- package/dist/features/admin/components/AdminTeamGuideView.js +33 -0
- package/dist/features/admin/components/AdminTrustGuideView.d.ts +1 -0
- package/dist/features/admin/components/AdminTrustGuideView.js +40 -0
- package/dist/features/admin/components/AdminUsersGuideView.d.ts +1 -0
- package/dist/features/admin/components/AdminUsersGuideView.js +38 -0
- 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 +3 -25
- package/dist/features/categories/components/CategoryProductsListing.js +6 -10
- package/dist/features/categories/components/index.d.ts +0 -2
- package/dist/features/categories/components/index.js +0 -1
- package/dist/features/categories/schemas/firestore.d.ts +9 -0
- package/dist/features/events/components/AdminEventEditorView.js +223 -269
- 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/grouped/repository/grouped-listings.repository.d.ts +9 -0
- package/dist/features/grouped/repository/grouped-listings.repository.js +12 -0
- 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/media/upload/MediaUploadField.js +1 -1
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +12 -6
- package/dist/features/products/components/AuctionsIndexListing.js +3 -1
- package/dist/features/products/components/MarketplaceBundleCard.d.ts +21 -0
- package/dist/features/products/components/MarketplaceBundleCard.js +56 -0
- 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 +18 -9
- package/dist/features/products/components/index.d.ts +2 -4
- package/dist/features/products/components/index.js +1 -2
- 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 +14 -1
- package/dist/features/reviews/components/index.d.ts +0 -2
- package/dist/features/reviews/components/index.js +0 -1
- package/dist/features/seller/components/BrandInlineSelect.js +23 -3
- package/dist/features/seller/components/CategoryInlineSelect.js +20 -3
- package/dist/features/seller/components/SellerAddressesView.js +3 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.d.ts +6 -0
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +124 -0
- package/dist/features/seller/components/SellerAnalyticsView.d.ts +2 -0
- package/dist/features/seller/components/SellerAnalyticsView.js +17 -9
- package/dist/features/seller/components/SellerBidsView.js +33 -3
- package/dist/features/seller/components/SellerBundlesView.d.ts +5 -0
- package/dist/features/seller/components/SellerBundlesView.js +104 -0
- package/dist/features/seller/components/SellerClassifiedView.d.ts +5 -0
- package/dist/features/seller/components/SellerClassifiedView.js +113 -0
- package/dist/features/seller/components/SellerDashboardView.js +19 -7
- package/dist/features/seller/components/SellerDigitalCodesView.d.ts +5 -0
- package/dist/features/seller/components/SellerDigitalCodesView.js +115 -0
- package/dist/features/seller/components/SellerGoogleReviewsView.d.ts +14 -0
- package/dist/features/seller/components/SellerGoogleReviewsView.js +95 -0
- package/dist/features/seller/components/SellerGroupedListingsView.d.ts +6 -0
- package/dist/features/seller/components/SellerGroupedListingsView.js +112 -0
- package/dist/features/seller/components/SellerLiveView.d.ts +5 -0
- package/dist/features/seller/components/SellerLiveView.js +113 -0
- package/dist/features/seller/components/SellerOrdersView.js +53 -2
- package/dist/features/seller/components/SellerPayoutMethodsView.d.ts +7 -0
- package/dist/features/seller/components/SellerPayoutMethodsView.js +120 -0
- package/dist/features/seller/components/SellerPayoutSettingsView.js +50 -12
- package/dist/features/seller/components/SellerProductShell.d.ts +3 -1
- package/dist/features/seller/components/SellerProductShell.js +35 -11
- 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 +22 -0
- package/dist/features/seller/components/SellerProductsView.d.ts +3 -1
- package/dist/features/seller/components/SellerProductsView.js +92 -49
- package/dist/features/seller/components/SellerReviewsView.js +67 -4
- package/dist/features/seller/components/SellerShippingConfigsView.d.ts +7 -0
- package/dist/features/seller/components/SellerShippingConfigsView.js +149 -0
- package/dist/features/seller/components/SellerShippingView.js +37 -10
- package/dist/features/seller/components/SellerSidebar.js +2 -1
- package/dist/features/seller/components/SellerStoreCategoriesView.d.ts +9 -0
- package/dist/features/seller/components/SellerStoreCategoriesView.js +122 -0
- package/dist/features/seller/components/SellerStorefrontView.d.ts +0 -7
- package/dist/features/seller/components/SellerStorefrontView.js +26 -29
- package/dist/features/seller/components/SellerTemplatesView.d.ts +10 -0
- package/dist/features/seller/components/SellerTemplatesView.js +265 -0
- package/dist/features/seller/components/index.d.ts +26 -0
- package/dist/features/seller/components/index.js +13 -0
- 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/api/[storeSlug]/reviews/route.js +49 -7
- package/dist/features/stores/components/InteractiveStoreCard.js +7 -3
- package/dist/features/stores/components/StoreCapabilitiesGuideView.d.ts +3 -0
- package/dist/features/stores/components/StoreCapabilitiesGuideView.js +101 -0
- package/dist/features/stores/components/StoreFinanceGuideView.d.ts +3 -0
- package/dist/features/stores/components/StoreFinanceGuideView.js +79 -0
- package/dist/features/stores/components/StoreGuideHubView.d.ts +5 -0
- package/dist/features/stores/components/StoreGuideHubView.js +89 -0
- package/dist/features/stores/components/StoreListingsGuideView.d.ts +3 -0
- package/dist/features/stores/components/StoreListingsGuideView.js +151 -0
- package/dist/features/stores/components/StoreOrdersGuideView.d.ts +3 -0
- package/dist/features/stores/components/StoreOrdersGuideView.js +122 -0
- package/dist/features/stores/components/StoreProductsListing.js +6 -10
- package/dist/features/stores/components/StoreReviewsListing.js +100 -23
- package/dist/features/stores/components/StoreSettingsGuideView.d.ts +3 -0
- package/dist/features/stores/components/StoreSettingsGuideView.js +56 -0
- package/dist/features/stores/components/index.d.ts +12 -1
- package/dist/features/stores/components/index.js +6 -1
- package/dist/features/stores/hooks/useStores.d.ts +5 -0
- package/dist/features/stores/hooks/useStores.js +10 -0
- package/dist/features/stores/manifest.js +1 -1
- package/dist/features/stores/schemas/firestore.d.ts +2 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +13 -2
- package/dist/index.d.ts +60 -9
- package/dist/index.js +66 -15
- package/dist/next/routing/route-map.d.ts +134 -0
- package/dist/next/routing/route-map.js +59 -0
- package/dist/providers/db-firebase/__tests__/filter-aliases.test.d.ts +1 -0
- package/dist/providers/db-firebase/__tests__/filter-aliases.test.js +93 -0
- package/dist/providers/db-firebase/sieve.d.ts +49 -0
- package/dist/providers/db-firebase/sieve.js +61 -8
- 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 +5 -0
- package/dist/repositories/index.js +7 -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/ListingToolbar.d.ts +8 -1
- package/dist/ui/components/ListingToolbar.js +4 -2
- 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/components/__tests__/BulkActionBar.test.d.ts +1 -0
- package/dist/ui/components/__tests__/BulkActionBar.test.js +96 -0
- package/dist/ui/components/__tests__/ListingToolbar.test.d.ts +1 -0
- package/dist/ui/components/__tests__/ListingToolbar.test.js +125 -0
- package/dist/ui/index.d.ts +9 -1
- package/dist/ui/index.js +4 -0
- package/package.json +8 -2
|
@@ -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,26 +107,34 @@ 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]);
|
|
129
|
-
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search products...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: PRODUCT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection,
|
|
135
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search products...", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: handleSearchKeyDown, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: PRODUCT_PUBLIC_SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: handleViewToggle, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection, toggles: [
|
|
136
|
+
{ label: "Show sold", active: showSold, onChange: (next) => table.set(TABLE_KEYS.SHOW_SOLD, next ? "true" : "") },
|
|
137
|
+
] }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
130
138
|
{
|
|
131
139
|
id: ACTION_ID.ADD_TO_CART,
|
|
132
140
|
label: ACTION_META[ACTION_ID.ADD_TO_CART].label,
|
|
@@ -135,8 +143,9 @@ export function ProductsIndexListing({ initialData }) {
|
|
|
135
143
|
onClick: () => {
|
|
136
144
|
const selected = products.filter((p) => selection.selectedIdSet.has(p.id));
|
|
137
145
|
selected.forEach((p) => {
|
|
138
|
-
|
|
139
|
-
|
|
146
|
+
const snapshot = { productTitle: p.title, productImage: p.mainImage, price: p.price, storeId: p.storeId, storeName: p.storeName };
|
|
147
|
+
localCart.add(p.id, 1, snapshot);
|
|
148
|
+
pushCartOp({ op: "add", productId: p.id, quantity: 1, ...snapshot });
|
|
140
149
|
});
|
|
141
150
|
showToast(`${selected.length} items added to cart`, "success");
|
|
142
151
|
selection.clearSelection();
|
|
@@ -15,6 +15,8 @@ export { PrizeRevealModal } from "./PrizeRevealModal";
|
|
|
15
15
|
export type { PrizeRevealModalProps, PrizeRevealResponse, } from "./PrizeRevealModal";
|
|
16
16
|
export { MarketplacePrizeDrawCard } from "./MarketplacePrizeDrawCard";
|
|
17
17
|
export type { MarketplacePrizeDrawCardProps, MarketplacePrizeDrawCardData, MarketplacePrizeDrawCardLabels, } from "./MarketplacePrizeDrawCard";
|
|
18
|
+
export { MarketplaceBundleCard } from "./MarketplaceBundleCard";
|
|
19
|
+
export type { MarketplaceBundleCardProps, MarketplaceBundleCardData, MarketplaceBundleCardLabels, } from "./MarketplaceBundleCard";
|
|
18
20
|
export { PrizeDrawsIndexListing } from "./PrizeDrawsIndexListing";
|
|
19
21
|
export type { PrizeDrawsIndexListingProps } from "./PrizeDrawsIndexListing";
|
|
20
22
|
export { PrizeDrawEntryActions } from "./PrizeDrawEntryActions";
|
|
@@ -30,14 +32,10 @@ export { BuyBar } from "./BuyBar";
|
|
|
30
32
|
export type { BuyBarProps } from "./BuyBar";
|
|
31
33
|
export { ProductsView } from "./ProductsView";
|
|
32
34
|
export type { ProductsViewProps } from "./ProductsView";
|
|
33
|
-
export { AuctionsView } from "./AuctionsView";
|
|
34
|
-
export type { AuctionsViewProps } from "./AuctionsView";
|
|
35
35
|
export { ProductDetailView } from "./ProductDetailView";
|
|
36
36
|
export type { ProductDetailViewProps } from "./ProductDetailView";
|
|
37
37
|
export { AuctionDetailView } from "./AuctionDetailView";
|
|
38
38
|
export type { AuctionDetailViewProps } from "./AuctionDetailView";
|
|
39
|
-
export { PreOrdersView } from "./PreOrdersView";
|
|
40
|
-
export type { PreOrdersViewProps } from "./PreOrdersView";
|
|
41
39
|
export { PreOrderDetailView } from "./PreOrderDetailView";
|
|
42
40
|
export type { PreOrderDetailViewProps } from "./PreOrderDetailView";
|
|
43
41
|
export { ProductInfo } from "./ProductInfo";
|
|
@@ -7,6 +7,7 @@ export { PrizeDrawItemsEditor } from "./PrizeDrawItemsEditor";
|
|
|
7
7
|
export { PrizeDrawCollage } from "./PrizeDrawCollage";
|
|
8
8
|
export { PrizeRevealModal } from "./PrizeRevealModal";
|
|
9
9
|
export { MarketplacePrizeDrawCard } from "./MarketplacePrizeDrawCard";
|
|
10
|
+
export { MarketplaceBundleCard } from "./MarketplaceBundleCard";
|
|
10
11
|
export { PrizeDrawsIndexListing } from "./PrizeDrawsIndexListing";
|
|
11
12
|
// PrizeDrawsListingView + PrizeDrawDetailPageView are server components that
|
|
12
13
|
// import productRepository (→ firebase-admin). Re-exporting them from this
|
|
@@ -20,10 +21,8 @@ export { FeatureBadge, FeatureBadgeList } from "./FeatureBadge";
|
|
|
20
21
|
export { ProductFeaturesProvider, useProductFeatures, } from "./ProductFeaturesContext";
|
|
21
22
|
export { BuyBar } from "./BuyBar";
|
|
22
23
|
export { ProductsView } from "./ProductsView";
|
|
23
|
-
export { AuctionsView } from "./AuctionsView";
|
|
24
24
|
export { ProductDetailView } from "./ProductDetailView";
|
|
25
25
|
export { AuctionDetailView } from "./AuctionDetailView";
|
|
26
|
-
export { PreOrdersView } from "./PreOrdersView";
|
|
27
26
|
export { PreOrderDetailView } from "./PreOrderDetailView";
|
|
28
27
|
export { ProductInfo } from "./ProductInfo";
|
|
29
28
|
export { ProductTabs } from "./ProductTabs";
|
|
@@ -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;
|
|
@@ -0,0 +1,115 @@
|
|
|
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, prepareForFirestore, } from "../../../providers/db-firebase";
|
|
9
|
+
import { CLAIMED_COUPONS_COLLECTION, createClaimedCouponId, } from "../schemas";
|
|
10
|
+
import { DatabaseError } from "../../../errors";
|
|
11
|
+
export class ClaimedCouponsRepository extends BaseRepository {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(CLAIMED_COUPONS_COLLECTION);
|
|
14
|
+
}
|
|
15
|
+
async findByUserAndCode(userId, couponCode) {
|
|
16
|
+
return this.findById(createClaimedCouponId(userId, couponCode));
|
|
17
|
+
}
|
|
18
|
+
/** Idempotent claim — returns the existing claim if one already exists. */
|
|
19
|
+
async claim(input) {
|
|
20
|
+
try {
|
|
21
|
+
const id = createClaimedCouponId(input.userId, input.couponCode);
|
|
22
|
+
const existing = await this.findById(id);
|
|
23
|
+
if (existing)
|
|
24
|
+
return existing;
|
|
25
|
+
const now = new Date();
|
|
26
|
+
const doc = {
|
|
27
|
+
id,
|
|
28
|
+
userId: input.userId,
|
|
29
|
+
couponId: input.couponId,
|
|
30
|
+
couponCode: input.couponCode.toUpperCase(),
|
|
31
|
+
source: input.source,
|
|
32
|
+
couponSnapshot: input.couponSnapshot,
|
|
33
|
+
status: "active",
|
|
34
|
+
expiresAt: input.expiresAt ?? null,
|
|
35
|
+
claimedAt: now,
|
|
36
|
+
updatedAt: now,
|
|
37
|
+
};
|
|
38
|
+
await this.db
|
|
39
|
+
.collection(this.collection)
|
|
40
|
+
.doc(id)
|
|
41
|
+
.set(prepareForFirestore(doc));
|
|
42
|
+
return doc;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
throw new DatabaseError("Failed to claim coupon", error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async listForUser(userId) {
|
|
49
|
+
try {
|
|
50
|
+
const snap = await this.db
|
|
51
|
+
.collection(this.collection)
|
|
52
|
+
.where("userId", "==", userId)
|
|
53
|
+
.get();
|
|
54
|
+
const now = Date.now();
|
|
55
|
+
const rows = [];
|
|
56
|
+
for (const docSnap of snap.docs) {
|
|
57
|
+
const raw = this.mapDoc(docSnap);
|
|
58
|
+
// Lazy expire — flip status when validity has passed. Best-effort
|
|
59
|
+
// write-back; consumers see the corrected status immediately.
|
|
60
|
+
if (raw.status === "active" &&
|
|
61
|
+
raw.expiresAt &&
|
|
62
|
+
new Date(raw.expiresAt).getTime() < now) {
|
|
63
|
+
raw.status = "expired";
|
|
64
|
+
void this.db
|
|
65
|
+
.collection(this.collection)
|
|
66
|
+
.doc(raw.id)
|
|
67
|
+
.update({ status: "expired", updatedAt: new Date() })
|
|
68
|
+
.catch(() => {
|
|
69
|
+
/* best-effort */
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
rows.push(raw);
|
|
73
|
+
}
|
|
74
|
+
return rows;
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
throw new DatabaseError("Failed to list claimed coupons", error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async markUsed(userId, couponCode, orderId) {
|
|
81
|
+
const id = createClaimedCouponId(userId, couponCode);
|
|
82
|
+
try {
|
|
83
|
+
await this.db
|
|
84
|
+
.collection(this.collection)
|
|
85
|
+
.doc(id)
|
|
86
|
+
.update({
|
|
87
|
+
status: "used",
|
|
88
|
+
usedAt: new Date(),
|
|
89
|
+
usedOrderId: orderId,
|
|
90
|
+
updatedAt: new Date(),
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
throw new DatabaseError("Failed to mark coupon used", error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async softRemove(userId, couponCode) {
|
|
98
|
+
const id = createClaimedCouponId(userId, couponCode);
|
|
99
|
+
try {
|
|
100
|
+
// Soft-delete = bump status to expired with a marker. We never physically
|
|
101
|
+
// delete so the audit trail (claim source + dates) is preserved.
|
|
102
|
+
await this.db
|
|
103
|
+
.collection(this.collection)
|
|
104
|
+
.doc(id)
|
|
105
|
+
.update({
|
|
106
|
+
status: "expired",
|
|
107
|
+
updatedAt: new Date(),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
throw new DatabaseError("Failed to remove claimed coupon", error);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export const claimedCouponsRepository = new ClaimedCouponsRepository();
|
|
@@ -70,6 +70,40 @@ export interface CouponUsageDocument {
|
|
|
70
70
|
lastUsedAt: Date;
|
|
71
71
|
orders: string[];
|
|
72
72
|
}
|
|
73
|
+
export type ClaimedCouponStatus = "active" | "expired" | "used";
|
|
74
|
+
export type ClaimedCouponSource = "manual" | "promo" | "spin" | "raffle" | "prize-draw";
|
|
75
|
+
export interface ClaimedCouponSnapshot {
|
|
76
|
+
name: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
type: CouponType;
|
|
79
|
+
scope: "admin" | "seller";
|
|
80
|
+
storeId?: string;
|
|
81
|
+
discount: DiscountConfig;
|
|
82
|
+
restrictions: RestrictionsConfig;
|
|
83
|
+
}
|
|
84
|
+
export interface ClaimedCouponDocument {
|
|
85
|
+
id: string;
|
|
86
|
+
userId: string;
|
|
87
|
+
couponId: string;
|
|
88
|
+
couponCode: string;
|
|
89
|
+
/** Source surface that initiated the claim — useful for analytics + UX. */
|
|
90
|
+
source: ClaimedCouponSource;
|
|
91
|
+
/** Denormalised snapshot so the wallet list renders without joins. */
|
|
92
|
+
couponSnapshot: ClaimedCouponSnapshot;
|
|
93
|
+
/** Lifecycle: active → used after redemption, → expired when validity.endDate passes. */
|
|
94
|
+
status: ClaimedCouponStatus;
|
|
95
|
+
/** Mirrors coupon.validity.endDate; null when the coupon is open-ended. */
|
|
96
|
+
expiresAt?: Date | null;
|
|
97
|
+
/** Set when status transitions to "used". */
|
|
98
|
+
usedAt?: Date;
|
|
99
|
+
/** Set when status transitions to "used". */
|
|
100
|
+
usedOrderId?: string;
|
|
101
|
+
claimedAt: Date;
|
|
102
|
+
updatedAt: Date;
|
|
103
|
+
}
|
|
104
|
+
export declare const CLAIMED_COUPONS_COLLECTION: "claimedCoupons";
|
|
105
|
+
export declare const CLAIMED_COUPONS_INDEXED_FIELDS: readonly ["userId", "couponCode", "status", "expiresAt"];
|
|
106
|
+
export declare function createClaimedCouponId(userId: string, couponCode: string): string;
|
|
73
107
|
export declare const COUPONS_COLLECTION: "coupons";
|
|
74
108
|
export declare const COUPON_USAGE_SUBCOLLECTION: "couponUsage";
|
|
75
109
|
export declare const COUPONS_INDEXED_FIELDS: readonly ["code", "validity.isActive", "validity.startDate", "validity.endDate", "type", "createdBy"];
|
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
* Coupons/Promotions Firestore Document Types & Constants
|
|
3
3
|
*/
|
|
4
4
|
import { generateCouponId } from "../../../utils/id-generators";
|
|
5
|
+
export const CLAIMED_COUPONS_COLLECTION = "claimedCoupons";
|
|
6
|
+
export const CLAIMED_COUPONS_INDEXED_FIELDS = [
|
|
7
|
+
"userId",
|
|
8
|
+
"couponCode",
|
|
9
|
+
"status",
|
|
10
|
+
"expiresAt",
|
|
11
|
+
];
|
|
12
|
+
export function createClaimedCouponId(userId, couponCode) {
|
|
13
|
+
// Mirrors the wishlist/history "claimed-{userSlug}-{code}" prefix convention.
|
|
14
|
+
const userSlug = userId.replace(/^user-/, "").toLowerCase();
|
|
15
|
+
const code = couponCode.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
16
|
+
return `claimed-${userSlug}-${code}`;
|
|
17
|
+
}
|
|
5
18
|
export const COUPONS_COLLECTION = "coupons";
|
|
6
19
|
export const COUPON_USAGE_SUBCOLLECTION = "couponUsage";
|
|
7
20
|
export const COUPONS_INDEXED_FIELDS = [
|
|
@@ -48,7 +61,7 @@ export const COUPON_TYPE_LABELS = {
|
|
|
48
61
|
};
|
|
49
62
|
export const DEFAULT_COUPON_DATA = {
|
|
50
63
|
usage: { currentUsage: 0 },
|
|
51
|
-
validity: { isActive:
|
|
64
|
+
validity: { isActive: true, startDate: new Date(), endDate: undefined },
|
|
52
65
|
restrictions: { firstTimeUserOnly: false, combineWithSellerCoupons: false },
|
|
53
66
|
stats: { totalUses: 0, totalRevenue: 0, totalDiscount: 0 },
|
|
54
67
|
};
|
|
@@ -6,8 +6,6 @@ export type { ReviewSummaryProps } from "./ReviewSummary";
|
|
|
6
6
|
export { ReviewSummary } from "./ReviewSummary";
|
|
7
7
|
export type { ViewReviewModalProps } from "./ReviewModal";
|
|
8
8
|
export { ViewReviewModal } from "./ReviewModal";
|
|
9
|
-
export { ReviewsListView } from "./ReviewsListView";
|
|
10
|
-
export type { ReviewsListViewProps } from "./ReviewsListView";
|
|
11
9
|
export { ReviewsIndexListing } from "./ReviewsIndexListing";
|
|
12
10
|
export type { ReviewsIndexListingProps } from "./ReviewsIndexListing";
|
|
13
11
|
export type { ReviewDetailPageViewProps } from "./ReviewDetailPageView";
|
|
@@ -2,5 +2,4 @@ export { ReviewCard, ReviewsList } from "./ReviewsList";
|
|
|
2
2
|
export { ReviewFilters, REVIEW_SORT_OPTIONS, REVIEW_FILTER_KEYS, REVIEW_ADMIN_SORT_OPTIONS, REVIEW_SELLER_SORT_OPTIONS, REVIEW_PUBLIC_SORT_OPTIONS, getReviewFilterKeys, getReviewSortOptions, } from "./ReviewFilters";
|
|
3
3
|
export { ReviewSummary } from "./ReviewSummary";
|
|
4
4
|
export { ViewReviewModal } from "./ReviewModal";
|
|
5
|
-
export { ReviewsListView } from "./ReviewsListView";
|
|
6
5
|
export { ReviewsIndexListing } from "./ReviewsIndexListing";
|
|
@@ -4,14 +4,16 @@ import { InlineCreateSelect } from "../../../ui/components/InlineCreateSelect";
|
|
|
4
4
|
import { BrandQuickCreateForm } from "../../admin/components/BrandQuickCreateForm";
|
|
5
5
|
import { apiClient } from "../../../http";
|
|
6
6
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
7
|
-
|
|
7
|
+
// Admin: paginated search via /api/admin/brands (admin/mod only).
|
|
8
|
+
// Response via successResponse({ data: items, total }) → apiClient unwraps to { data, total }.
|
|
9
|
+
async function loadAdminBrandOptions(query, page) {
|
|
8
10
|
const params = new URLSearchParams({
|
|
9
11
|
q: query,
|
|
10
12
|
page: String(page),
|
|
11
13
|
pageSize: "20",
|
|
12
14
|
});
|
|
13
15
|
const res = await apiClient.get(`${ADMIN_ENDPOINTS.BRANDS}?${params}`);
|
|
14
|
-
const raw = res.
|
|
16
|
+
const raw = res.data ?? res.items ?? [];
|
|
15
17
|
const items = raw.map((b) => ({
|
|
16
18
|
value: String(b.id ?? ""),
|
|
17
19
|
label: String(b.name ?? ""),
|
|
@@ -19,8 +21,26 @@ async function loadBrandOptions(query, page) {
|
|
|
19
21
|
const total = res.total ?? items.length;
|
|
20
22
|
return { items, hasMore: page * 20 < total, nextPage: page + 1 };
|
|
21
23
|
}
|
|
24
|
+
// Seller/public: unauthenticated /api/brands endpoint returns { items, total }.
|
|
25
|
+
// Client-side query filtering so all sellers can access the brand list.
|
|
26
|
+
async function loadPublicBrandOptions(query, page) {
|
|
27
|
+
const params = new URLSearchParams({
|
|
28
|
+
page: String(page),
|
|
29
|
+
pageSize: "100",
|
|
30
|
+
active: "true",
|
|
31
|
+
});
|
|
32
|
+
const res = await apiClient.get(`/api/brands?${params}`);
|
|
33
|
+
const all = (res.items ?? []).map((b) => ({
|
|
34
|
+
value: String(b.id ?? ""),
|
|
35
|
+
label: String(b.name ?? ""),
|
|
36
|
+
}));
|
|
37
|
+
const q = query.trim().toLowerCase();
|
|
38
|
+
const items = q ? all.filter((b) => b.label.toLowerCase().includes(q)) : all;
|
|
39
|
+
return { items, hasMore: false, nextPage: page + 1 };
|
|
40
|
+
}
|
|
22
41
|
export function BrandInlineSelect({ value, onChange, placeholder = "Search brands…", disabled, allowCreate = true, }) {
|
|
23
|
-
|
|
42
|
+
const loadOptions = allowCreate ? loadAdminBrandOptions : loadPublicBrandOptions;
|
|
43
|
+
return (_jsx(InlineCreateSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadOptions, placeholder: placeholder, disabled: disabled, createLabel: "brand", drawerTitle: "Create Brand", renderCreateForm: allowCreate
|
|
24
44
|
? ({ onCreated, onCancel }) => (_jsx(BrandQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel }))
|
|
25
45
|
: undefined }));
|
|
26
46
|
}
|
|
@@ -4,7 +4,9 @@ import { InlineCreateSelect } from "../../../ui/components/InlineCreateSelect";
|
|
|
4
4
|
import { CategoryQuickCreateForm } from "../../admin/components/CategoryQuickCreateForm";
|
|
5
5
|
import { apiClient } from "../../../http";
|
|
6
6
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
7
|
-
|
|
7
|
+
// Admin: paginated + server-side search via /api/admin/categories (admin/mod only).
|
|
8
|
+
// Response via successResponse({ data: items, total }) → apiClient unwraps to { data, total, ... }.
|
|
9
|
+
async function loadAdminCategoryOptions(query, page) {
|
|
8
10
|
const params = new URLSearchParams({
|
|
9
11
|
q: query,
|
|
10
12
|
page: String(page),
|
|
@@ -12,15 +14,30 @@ async function loadCategoryOptions(query, page) {
|
|
|
12
14
|
flat: "true",
|
|
13
15
|
});
|
|
14
16
|
const res = await apiClient.get(`${ADMIN_ENDPOINTS.CATEGORIES}?${params}`);
|
|
15
|
-
const items = (res.
|
|
17
|
+
const items = (res.data ?? []).map((c) => ({
|
|
16
18
|
value: String(c.id ?? ""),
|
|
17
19
|
label: String(c.name ?? ""),
|
|
18
20
|
}));
|
|
19
21
|
const total = res.total ?? items.length;
|
|
20
22
|
return { items, hasMore: page * 20 < total, nextPage: page + 1 };
|
|
21
23
|
}
|
|
24
|
+
// Seller: fetch all flat categories from the public unauthenticated endpoint,
|
|
25
|
+
// filter client-side by query. Public endpoint returns { success, data: CategoryItem[] }.
|
|
26
|
+
async function loadPublicCategoryOptions(query, _page) {
|
|
27
|
+
const params = new URLSearchParams({ flat: "true", pageSize: "200" });
|
|
28
|
+
const res = await fetch(`/api/categories?${params}`, { credentials: "include" });
|
|
29
|
+
const json = await res.json().catch(() => ({}));
|
|
30
|
+
const all = (json.data ?? []).map((c) => ({
|
|
31
|
+
value: String(c.id ?? ""),
|
|
32
|
+
label: String(c.name ?? ""),
|
|
33
|
+
}));
|
|
34
|
+
const q = query.trim().toLowerCase();
|
|
35
|
+
const items = q ? all.filter((c) => c.label.toLowerCase().includes(q)) : all;
|
|
36
|
+
return { items, hasMore: false, nextPage: 2 };
|
|
37
|
+
}
|
|
22
38
|
export function CategoryInlineSelect({ value, onChange, placeholder = "Search categories…", disabled, allowCreate = false, }) {
|
|
23
|
-
|
|
39
|
+
const loadOptions = allowCreate ? loadAdminCategoryOptions : loadPublicCategoryOptions;
|
|
40
|
+
return (_jsx(InlineCreateSelect, { value: value || null, onChange: (v) => onChange(v ?? ""), loadOptions: loadOptions, placeholder: placeholder, disabled: disabled, createLabel: "category", drawerTitle: "Create Category", renderCreateForm: allowCreate
|
|
24
41
|
? ({ onCreated, onCancel }) => (_jsx(CategoryQuickCreateForm, { onSaved: (id, name) => onCreated({ value: id, label: name }), onCancel: onCancel }))
|
|
25
42
|
: undefined }));
|
|
26
43
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useState } from "react";
|
|
4
4
|
import { MapPin, Pencil, Plus, Trash2, Star } from "lucide-react";
|
|
5
|
-
import { Button, Div, Heading, SideDrawer, Text } from "../../../ui";
|
|
5
|
+
import { Button, Div, Heading, Row, SideDrawer, Text } from "../../../ui";
|
|
6
6
|
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
7
7
|
const INPUT_CLS = "w-full rounded-lg border border-zinc-300 dark:border-slate-600 bg-white dark:bg-slate-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 dark:placeholder-zinc-500 focus:outline-none focus:ring-2 focus:ring-[var(--appkit-color-primary)]";
|
|
8
8
|
const CLS_GRID_2_COL = "grid grid-cols-2 gap-3";
|
|
@@ -48,6 +48,7 @@ function AddressCard({ address, onEdit, onDelete, }) {
|
|
|
48
48
|
// ---------------------------------------------------------------------------
|
|
49
49
|
export function SellerAddressesView({ apiBase = SELLER_ENDPOINTS.STORE_ADDRESSES, }) {
|
|
50
50
|
const [addresses, setAddresses] = useState([]);
|
|
51
|
+
const [listView, setListView] = useState("table");
|
|
51
52
|
const [isLoading, setIsLoading] = useState(true);
|
|
52
53
|
const [errorMessage, setErrorMessage] = useState(null);
|
|
53
54
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
@@ -143,5 +144,5 @@ export function SellerAddressesView({ apiBase = SELLER_ENDPOINTS.STORE_ADDRESSES
|
|
|
143
144
|
};
|
|
144
145
|
const set = (key, value) => setDraft((p) => ({ ...p, [key]: value }));
|
|
145
146
|
const handleTextField = (key) => (e) => set(key, e.target.value);
|
|
146
|
-
return (_jsxs("div", { className: "min-h-screen", children: [_jsxs("div", { className: "sticky z-10 bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-4 py-3 flex items-center justify-between", style: { top: "var(--header-height, 0px)" }, children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Pickup Addresses" }), _jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400 mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Button, { size: "sm", onClick: openAdd, className: "flex items-center gap-1.5", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { children: "Add Address" })] })] }), _jsxs("div", { className: "py-6 px-4 sm:px-6 max-w-2xl", children: [errorMessage && (_jsx(Div, { className: "mb-4 rounded-xl border border-red-200 bg-red-50 dark:bg-red-950/40 dark:border-red-900/60 px-4 py-3 text-sm text-red-700 dark:text-red-200", children: errorMessage })), isLoading ? (_jsx("div", { className: "flex justify-center py-16", children: _jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-[var(--appkit-color-primary)] border-t-transparent" }) })) : addresses.length === 0 ? (_jsxs(Div, { className: "rounded-xl border-2 border-dashed border-zinc-200 dark:border-slate-700 py-16 flex flex-col items-center gap-3", children: [_jsx(MapPin, { className: "h-8 w-8 text-zinc-300 dark:text-slate-600" }), _jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "No pickup addresses yet" }), _jsx(Button, { size: "sm", variant: "outline", onClick: openAdd, children: "Add your first address" })] })) : (_jsx("div", { className: "grid gap-3", children: addresses.map((addr) => (_jsx("div", { className: deletingId === addr.id ? "opacity-50 pointer-events-none" : "", children: _jsx(AddressCard, { address: addr, onEdit: () => openEdit(addr), onDelete: () => handleDelete(addr) }) }, addr.id))) }))] }), _jsx(SideDrawer, { isOpen: drawerOpen, onClose: closeDrawer, title: editingId ? "Edit Address" : "Add Address", footer: _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", onClick: closeDrawer, className: "flex-1", children: "Cancel" }), _jsx(Button, { onClick: handleSave, disabled: saving, className: "flex-1", children: saving ? "Saving…" : editingId ? "Save Changes" : "Add Address" })] }), children: _jsxs("div", { className: "space-y-4 py-1", children: [saveError && (_jsx(Div, { className: "rounded-lg border border-red-200 bg-red-50 dark:bg-red-950/40 dark:border-red-900/60 px-3 py-2 text-sm text-red-700 dark:text-red-200", children: saveError })), _jsx(Field, { label: "Label *", hint: "e.g. Warehouse, Shop, Home", children: _jsx("input", { type: "text", value: draft.label, onChange: handleTextField("label"), placeholder: "Warehouse", maxLength: 60, className: INPUT_CLS }) }), _jsxs("div", { className: CLS_GRID_2_COL, children: [_jsx(Field, { label: "Full Name *", children: _jsx("input", { type: "text", value: draft.fullName, onChange: handleTextField("fullName"), placeholder: "Ravi Kumar", maxLength: 100, className: INPUT_CLS }) }), _jsx(Field, { label: "Phone *", children: _jsx("input", { type: "tel", value: draft.phone, onChange: handleTextField("phone"), placeholder: "+91 98765 43210", maxLength: 20, className: INPUT_CLS }) })] }), _jsx(Field, { label: "Address Line 1 *", children: _jsx("input", { type: "text", value: draft.addressLine1, onChange: handleTextField("addressLine1"), placeholder: "Shop 12, Main Market", maxLength: 200, className: INPUT_CLS }) }), _jsx(Field, { label: "Address Line 2", children: _jsx("input", { type: "text", value: draft.addressLine2, onChange: handleTextField("addressLine2"), placeholder: "Building / Floor (optional)", maxLength: 200, className: INPUT_CLS }) }), _jsx(Field, { label: "Landmark", children: _jsx("input", { type: "text", value: draft.landmark, onChange: handleTextField("landmark"), placeholder: "Near metro station (optional)", maxLength: 100, className: INPUT_CLS }) }), _jsxs("div", { className: CLS_GRID_2_COL, children: [_jsx(Field, { label: "City *", children: _jsx("input", { type: "text", value: draft.city, onChange: handleTextField("city"), placeholder: "Mumbai", maxLength: 100, className: INPUT_CLS }) }), _jsx(Field, { label: "State *", children: _jsx("input", { type: "text", value: draft.state, onChange: handleTextField("state"), placeholder: "Maharashtra", maxLength: 100, className: INPUT_CLS }) })] }), _jsxs("div", { className: CLS_GRID_2_COL, children: [_jsx(Field, { label: "Postal Code *", children: _jsx("input", { type: "text", value: draft.postalCode, onChange: handleTextField("postalCode"), placeholder: "400001", maxLength: 10, className: INPUT_CLS }) }), _jsx(Field, { label: "Country *", children: _jsx("input", { type: "text", value: draft.country, onChange: handleTextField("country"), placeholder: "India", maxLength: 60, className: INPUT_CLS }) })] }), _jsxs("label", { className: "flex items-center gap-3 cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: draft.isDefault, onChange: (e) => set("isDefault", e.target.checked), className: "h-4 w-4 rounded border-zinc-300 dark:border-slate-600 text-[var(--appkit-color-primary)] focus:ring-[var(--appkit-color-primary)]" }), _jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: "Set as default pickup address" })] })] }) })] }));
|
|
147
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsxs("div", { className: "sticky z-10 bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-4 py-3 flex items-center justify-between", style: { top: "var(--header-height, 0px)" }, children: [_jsxs(_Fragment, { children: [_jsx(Heading, { level: 2, className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Pickup Addresses" }), _jsx(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400 mt-0.5", children: "Manage your store's pickup and return locations" })] }), _jsxs(Button, { size: "sm", onClick: openAdd, className: "flex items-center gap-1.5", children: [_jsx(Plus, { className: "h-4 w-4" }), _jsx("span", { children: "Add Address" })] })] }), _jsxs("div", { className: "py-6 px-4 sm:px-6 max-w-2xl", children: [errorMessage && (_jsx(Div, { className: "mb-4 rounded-xl border border-red-200 bg-red-50 dark:bg-red-950/40 dark:border-red-900/60 px-4 py-3 text-sm text-red-700 dark:text-red-200", children: errorMessage })), isLoading ? (_jsx("div", { className: "flex justify-center py-16", children: _jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-[var(--appkit-color-primary)] border-t-transparent" }) })) : addresses.length === 0 ? (_jsxs(Div, { className: "rounded-xl border-2 border-dashed border-zinc-200 dark:border-slate-700 py-16 flex flex-col items-center gap-3", children: [_jsx(MapPin, { className: "h-8 w-8 text-zinc-300 dark:text-slate-600" }), _jsx(Text, { className: "text-sm text-zinc-500 dark:text-zinc-400", children: "No pickup addresses yet" }), _jsx(Button, { size: "sm", variant: "outline", onClick: openAdd, children: "Add your first address" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Row, { className: "justify-end mb-2", children: [_jsx(Button, { size: "sm", variant: listView === "table" ? "primary" : "ghost", onClick: () => setListView("table"), children: "Table" }), _jsx(Button, { size: "sm", variant: listView === "cards" ? "primary" : "ghost", onClick: () => setListView("cards"), children: "Cards" })] }), listView === "cards" ? (_jsx("div", { className: "grid gap-3", children: addresses.map((addr) => (_jsx("div", { className: deletingId === addr.id ? "opacity-50 pointer-events-none" : "", children: _jsx(AddressCard, { address: addr, onEdit: () => openEdit(addr), onDelete: () => handleDelete(addr) }) }, addr.id))) })) : (_jsx("div", { className: "overflow-x-auto rounded-lg border border-zinc-200 dark:border-slate-700", children: _jsxs("table", { className: "w-full text-sm", children: [_jsx("thead", { className: "bg-zinc-50 dark:bg-slate-800", children: _jsxs("tr", { children: [_jsx("th", { className: "text-left px-3 py-2 font-semibold", children: "Label" }), _jsx("th", { className: "text-left px-3 py-2 font-semibold", children: "Name" }), _jsx("th", { className: "text-left px-3 py-2 font-semibold", children: "City" }), _jsx("th", { className: "text-left px-3 py-2 font-semibold", children: "Phone" }), _jsx("th", { className: "text-right px-3 py-2 font-semibold", children: "Actions" })] }) }), _jsx("tbody", { children: addresses.map((addr) => (_jsxs("tr", { className: `border-t border-zinc-100 dark:border-slate-700 ${deletingId === addr.id ? "opacity-50" : ""}`, children: [_jsx("td", { className: "px-3 py-2", children: addr.label }), _jsx("td", { className: "px-3 py-2", children: addr.fullName }), _jsxs("td", { className: "px-3 py-2", children: [addr.city, ", ", addr.state] }), _jsx("td", { className: "px-3 py-2 tabular-nums", children: addr.phone }), _jsx("td", { className: "px-3 py-2 text-right", children: _jsxs(Row, { className: "gap-1 justify-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onClick: () => openEdit(addr), children: "Edit" }), _jsx(Button, { size: "sm", variant: "ghost", onClick: () => handleDelete(addr), children: "Delete" })] }) })] }, addr.id))) })] }) }))] }))] }), _jsx(SideDrawer, { isOpen: drawerOpen, onClose: closeDrawer, title: editingId ? "Edit Address" : "Add Address", footer: _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { variant: "outline", onClick: closeDrawer, className: "flex-1", children: "Cancel" }), _jsx(Button, { onClick: handleSave, disabled: saving, className: "flex-1", children: saving ? "Saving…" : editingId ? "Save Changes" : "Add Address" })] }), children: _jsxs("div", { className: "space-y-4 py-1", children: [saveError && (_jsx(Div, { className: "rounded-lg border border-red-200 bg-red-50 dark:bg-red-950/40 dark:border-red-900/60 px-3 py-2 text-sm text-red-700 dark:text-red-200", children: saveError })), _jsx(Field, { label: "Label *", hint: "e.g. Warehouse, Shop, Home", children: _jsx("input", { type: "text", value: draft.label, onChange: handleTextField("label"), placeholder: "Warehouse", maxLength: 60, className: INPUT_CLS }) }), _jsxs("div", { className: CLS_GRID_2_COL, children: [_jsx(Field, { label: "Full Name *", children: _jsx("input", { type: "text", value: draft.fullName, onChange: handleTextField("fullName"), placeholder: "Ravi Kumar", maxLength: 100, className: INPUT_CLS }) }), _jsx(Field, { label: "Phone *", children: _jsx("input", { type: "tel", value: draft.phone, onChange: handleTextField("phone"), placeholder: "+91 98765 43210", maxLength: 20, className: INPUT_CLS }) })] }), _jsx(Field, { label: "Address Line 1 *", children: _jsx("input", { type: "text", value: draft.addressLine1, onChange: handleTextField("addressLine1"), placeholder: "Shop 12, Main Market", maxLength: 200, className: INPUT_CLS }) }), _jsx(Field, { label: "Address Line 2", children: _jsx("input", { type: "text", value: draft.addressLine2, onChange: handleTextField("addressLine2"), placeholder: "Building / Floor (optional)", maxLength: 200, className: INPUT_CLS }) }), _jsx(Field, { label: "Landmark", children: _jsx("input", { type: "text", value: draft.landmark, onChange: handleTextField("landmark"), placeholder: "Near metro station (optional)", maxLength: 100, className: INPUT_CLS }) }), _jsxs("div", { className: CLS_GRID_2_COL, children: [_jsx(Field, { label: "City *", children: _jsx("input", { type: "text", value: draft.city, onChange: handleTextField("city"), placeholder: "Mumbai", maxLength: 100, className: INPUT_CLS }) }), _jsx(Field, { label: "State *", children: _jsx("input", { type: "text", value: draft.state, onChange: handleTextField("state"), placeholder: "Maharashtra", maxLength: 100, className: INPUT_CLS }) })] }), _jsxs("div", { className: CLS_GRID_2_COL, children: [_jsx(Field, { label: "Postal Code *", children: _jsx("input", { type: "text", value: draft.postalCode, onChange: handleTextField("postalCode"), placeholder: "400001", maxLength: 10, className: INPUT_CLS }) }), _jsx(Field, { label: "Country *", children: _jsx("input", { type: "text", value: draft.country, onChange: handleTextField("country"), placeholder: "India", maxLength: 60, className: INPUT_CLS }) })] }), _jsxs("label", { className: "flex items-center gap-3 cursor-pointer", children: [_jsx("input", { type: "checkbox", checked: draft.isDefault, onChange: (e) => set("isDefault", e.target.checked), className: "h-4 w-4 rounded border-zinc-300 dark:border-slate-600 text-[var(--appkit-color-primary)] focus:ring-[var(--appkit-color-primary)]" }), _jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: "Set as default pickup address" })] })] }) })] }));
|
|
147
148
|
}
|