@mohasinac/appkit 4.10.0 → 4.11.0
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/DashboardLayoutClient.d.ts +6 -1
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +4 -1
- package/dist/_internal/client/features/lottery/LotteryPullForm.d.ts +1 -1
- package/dist/_internal/client/features/lottery/LotteryPullForm.js +2 -2
- package/dist/_internal/server/features/admin/daily-digest-actions.d.ts +18 -0
- package/dist/_internal/server/features/admin/daily-digest-actions.js +37 -0
- package/dist/_internal/server/features/audit-log/actions.d.ts +25 -0
- package/dist/_internal/server/features/audit-log/actions.js +38 -0
- package/dist/_internal/server/features/brands/actions.js +0 -2
- package/dist/_internal/server/features/brands/og.d.ts +4 -1
- package/dist/_internal/server/features/brands/og.js +1 -1
- package/dist/_internal/server/features/grouped/data.d.ts +4 -0
- package/dist/_internal/server/features/grouped/data.js +24 -0
- package/dist/_internal/server/features/grouped/index.d.ts +1 -1
- package/dist/_internal/server/features/grouped/index.js +1 -1
- package/dist/_internal/server/features/orders/actions.js +9 -0
- package/dist/_internal/server/features/orders/adapters.js +25 -0
- package/dist/_internal/server/functions/scheduled.d.ts +3 -1
- package/dist/_internal/server/functions/scheduled.js +17 -1
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +40 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +164 -0
- package/dist/_internal/server/jobs/core/hardBanCascade.js +11 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.d.ts +12 -0
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +85 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.js +16 -0
- package/dist/_internal/server/jobs/handlers/dailyStatusDigest.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/dailyStatusDigest.js +4 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/index.js +3 -0
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.js +2 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +27 -0
- package/dist/_internal/server/notifications/channel-health.js +101 -0
- package/dist/_internal/shared/features/brands/schema.d.ts +0 -6
- package/dist/_internal/shared/features/brands/schema.js +0 -1
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +7 -0
- package/dist/_internal/shared/features/categories/bundle-schemas.js +3 -0
- package/dist/_internal/shared/features/orders/schema.d.ts +8 -8
- package/dist/client.d.ts +21 -6
- package/dist/client.js +13 -3
- package/dist/constants/api-endpoints.d.ts +6 -9
- package/dist/constants/api-endpoints.js +2 -3
- package/dist/contracts/client-auth.d.ts +3 -7
- package/dist/contracts/email.d.ts +1 -0
- package/dist/features/account/components/AddressForm.js +24 -2
- package/dist/features/account/components/BecomeSellerView.d.ts +1 -2
- package/dist/features/account/components/BecomeSellerView.js +1 -1
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/account/components/UserBidsView.d.ts +1 -0
- package/dist/features/account/components/UserBidsView.js +47 -0
- package/dist/features/account/components/UserOrdersView.d.ts +4 -11
- package/dist/features/account/components/UserOrdersView.js +54 -4
- package/dist/features/account/components/UserReturnsView.d.ts +4 -9
- package/dist/features/account/components/UserReturnsView.js +37 -3
- package/dist/features/account/components/index.d.ts +3 -2
- package/dist/features/account/components/index.js +1 -0
- package/dist/features/admin/actions/admin-actions.js +22 -0
- package/dist/features/admin/actions/admin-coupon-actions.js +10 -0
- package/dist/features/admin/actions/notification-actions.js +4 -3
- package/dist/features/admin/components/AdminAdsView.d.ts +2 -3
- package/dist/features/admin/components/AdminAdsView.js +86 -89
- package/dist/features/admin/components/AdminAllEventEntriesView.js +18 -3
- package/dist/features/admin/components/AdminAuditLogView.d.ts +11 -0
- package/dist/features/admin/components/AdminAuditLogView.js +95 -0
- package/dist/features/admin/components/AdminBlogEditorView.js +37 -3
- package/dist/features/admin/components/AdminBrandEditorView.js +10 -6
- package/dist/features/admin/components/AdminBundleEditorView.js +18 -2
- package/dist/features/admin/components/AdminCarouselEditorView.js +2 -2
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +2 -2
- package/dist/features/admin/components/AdminCartsView.js +34 -4
- package/dist/features/admin/components/AdminCategoryEditorView.js +5 -4
- package/dist/features/admin/components/AdminCouponEditorView.js +2 -2
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -2
- package/dist/features/admin/components/AdminHistoryView.js +11 -1
- package/dist/features/admin/components/AdminNotificationsView.js +27 -1
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +10 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
- package/dist/features/admin/components/AdminOrdersView.js +92 -24
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.d.ts +7 -0
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +47 -0
- package/dist/features/admin/components/AdminPayoutsView.js +15 -31
- package/dist/features/admin/components/AdminProductEditorView.js +26 -3
- package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
- package/dist/features/admin/components/AdminStoreAddressesView.js +22 -2
- package/dist/features/admin/components/AdminStoresView.js +7 -0
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -2
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -2
- package/dist/features/admin/components/AdminUserEditorView.d.ts +2 -2
- package/dist/features/admin/components/AdminUserEditorView.js +4 -2
- package/dist/features/admin/components/AdminUsersView.js +1 -1
- package/dist/features/admin/components/AdminWishlistsView.js +14 -1
- package/dist/features/admin/components/DataListingView.d.ts +2 -2
- package/dist/features/admin/components/ViewAuditLogEntryModal.d.ts +19 -0
- package/dist/features/admin/components/ViewAuditLogEntryModal.js +22 -0
- package/dist/features/admin/components/ViewNotificationModal.d.ts +21 -0
- package/dist/features/admin/components/ViewNotificationModal.js +15 -0
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.d.ts +4 -7
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +56 -36
- package/dist/features/admin/components/index.d.ts +8 -1
- package/dist/features/admin/components/index.js +4 -0
- package/dist/features/admin/constants/filter-tabs.d.ts +49 -0
- package/dist/features/admin/constants/filter-tabs.js +27 -0
- package/dist/features/admin/hooks/useAdminListing.d.ts +6 -0
- package/dist/features/admin/hooks/useAdminListing.js +3 -1
- package/dist/features/admin/hooks/useAdminListingData.d.ts +9 -1
- package/dist/features/admin/hooks/useAdminListingData.js +9 -2
- package/dist/features/admin/repository/site-settings.repository.d.ts +5 -0
- package/dist/features/admin/schemas/firestore.d.ts +12 -0
- package/dist/features/analytics/types.d.ts +1 -1
- package/dist/features/analytics/types.js +8 -0
- package/dist/features/auctions/schemas/index.d.ts +5 -0
- package/dist/features/audit-log/repository/audit-log.repository.d.ts +45 -0
- package/dist/features/audit-log/repository/audit-log.repository.js +30 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +59 -0
- package/dist/features/audit-log/schemas/firestore.js +35 -0
- package/dist/features/auth/actions/index.d.ts +0 -1
- package/dist/features/auth/actions/index.js +0 -1
- package/dist/features/auth/hooks/useAuth.d.ts +8 -35
- package/dist/features/auth/hooks/useAuth.js +21 -37
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +2 -0
- package/dist/features/blog/schemas/index.d.ts +32 -0
- package/dist/features/catalogue/components/AdminCatalogueApprovalsView.js +31 -3
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +2 -1
- package/dist/features/categories/components/BrandDetailPageView.js +14 -12
- package/dist/features/categories/components/CategoryDetailPageView.js +7 -3
- package/dist/features/categories/components/CategoryHighlightsAndFaqSection.d.ts +14 -0
- package/dist/features/categories/components/CategoryHighlightsAndFaqSection.js +22 -0
- package/dist/features/categories/components/CategoryProductsView.d.ts +1 -5
- package/dist/features/categories/components/CategoryProductsView.js +1 -1
- package/dist/features/categories/components/CategorySelectorCreate.js +16 -2
- package/dist/features/categories/schemas/firestore.d.ts +19 -2
- package/dist/features/categories/schemas/index.d.ts +24 -3
- package/dist/features/categories/schemas/index.js +3 -1
- package/dist/features/categories/types/index.d.ts +7 -0
- package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +17 -3
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +101 -22
- package/dist/features/contact/components/ContactForm.js +17 -4
- package/dist/features/contact/email.d.ts +1 -0
- package/dist/features/contact/email.js +3 -0
- package/dist/features/events/components/AdminEventEditorView.js +54 -2
- package/dist/features/events/components/AdminEventEntriesView.d.ts +2 -16
- package/dist/features/events/components/AdminEventEntriesView.js +56 -89
- package/dist/features/events/components/EventFormDrawer.js +5 -1
- package/dist/features/events/components/EventRaffleEntryForm.d.ts +1 -3
- package/dist/features/events/components/EventRaffleEntryForm.js +1 -1
- package/dist/features/events/schemas/index.d.ts +22 -0
- package/dist/features/grouped/repository/grouped-listings.repository.d.ts +4 -0
- package/dist/features/grouped/repository/grouped-listings.repository.js +18 -0
- package/dist/features/homepage/components/CharacterHotspotForm.js +202 -187
- package/dist/features/homepage/components/SectionCarousel.d.ts +1 -7
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/layout/TitleBarLayout.d.ts +1 -1
- package/dist/features/layout/TitleBarLayout.js +3 -3
- package/dist/features/media/MediaImage.js +39 -3
- package/dist/features/media/types/index.d.ts +10 -0
- package/dist/features/media/types/index.js +1 -0
- package/dist/features/media/upload/MediaUploadField.d.ts +7 -1
- package/dist/features/media/upload/MediaUploadField.js +11 -7
- package/dist/features/media/upload/video-poster.d.ts +2 -0
- package/dist/features/media/upload/video-poster.js +16 -1
- package/dist/features/media/upload/video-poster.test.js +1 -1
- package/dist/features/orders/components/OrderStatusTimeline.d.ts +12 -0
- package/dist/features/orders/components/OrderStatusTimeline.js +55 -0
- package/dist/features/orders/components/index.d.ts +2 -0
- package/dist/features/orders/components/index.js +1 -0
- package/dist/features/orders/constants/payment-window.d.ts +25 -0
- package/dist/features/orders/constants/payment-window.js +25 -0
- package/dist/features/orders/repository/orders.repository.d.ts +32 -0
- package/dist/features/orders/repository/orders.repository.js +56 -0
- package/dist/features/orders/schemas/index.d.ts +24 -24
- package/dist/features/orders/types/index.d.ts +33 -0
- package/dist/features/pre-orders/schemas/index.d.ts +5 -0
- package/dist/features/products/components/BidHistory.js +9 -1
- package/dist/features/products/components/GroupSettingsPanel.js +25 -3
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +1 -4
- package/dist/features/products/components/PrizeDrawEntryActions.js +1 -1
- package/dist/features/products/components/ProductDetailActions.d.ts +1 -2
- package/dist/features/products/components/ProductDetailActions.js +1 -1
- package/dist/features/products/components/ProductGrid.js +15 -1
- package/dist/features/products/constants/action-defs.d.ts +1 -0
- package/dist/features/products/constants/action-defs.js +1 -0
- package/dist/features/products/schemas/index.d.ts +5 -0
- package/dist/features/promotions/components/CouponsIndexListing.d.ts +1 -3
- package/dist/features/promotions/components/CouponsIndexListing.js +1 -1
- package/dist/features/search/components/SearchView.d.ts +1 -3
- package/dist/features/search/components/SearchView.js +1 -1
- package/dist/features/seller/components/QuickProductForm.js +2 -1
- package/dist/features/seller/components/SellerBidsView.js +66 -107
- package/dist/features/seller/components/SellerGoogleReviewsView.js +17 -1
- package/dist/features/seller/components/SellerOrdersView.d.ts +15 -3
- package/dist/features/seller/components/SellerOrdersView.js +134 -163
- package/dist/features/seller/components/SellerPayoutMethodsView.js +60 -48
- package/dist/features/seller/components/SellerPayoutSettingsView.js +69 -4
- package/dist/features/seller/components/SellerPayoutsView.d.ts +12 -0
- package/dist/features/seller/components/SellerPayoutsView.js +25 -5
- package/dist/features/seller/components/SellerProductShell.d.ts +10 -3
- package/dist/features/seller/components/SellerProductShell.js +89 -36
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +2 -7
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +4 -4
- package/dist/features/seller/components/SellerProductsView.d.ts +2 -4
- package/dist/features/seller/components/SellerProductsView.js +131 -189
- package/dist/features/seller/components/SellerReviewsView.js +95 -133
- package/dist/features/seller/components/SellerShippingConfigsView.js +47 -67
- package/dist/features/seller/components/SellerShippingView.js +41 -4
- package/dist/features/seller/components/SellerStoreCategoriesView.js +51 -59
- package/dist/features/seller/components/SellerStorefrontView.d.ts +6 -1
- package/dist/features/seller/components/SellerStorefrontView.js +42 -5
- package/dist/features/seller/components/index.d.ts +2 -2
- package/dist/features/seller/components/index.js +2 -2
- package/dist/features/shell/FormShell.d.ts +24 -4
- package/dist/features/shell/FormShell.js +12 -6
- package/dist/features/shell/QuickFormDrawer.js +67 -12
- package/dist/features/shell/StepForm.d.ts +30 -1
- package/dist/features/shell/StepForm.js +47 -4
- package/dist/features/shell/index.d.ts +1 -1
- package/dist/features/shell/index.js +1 -1
- package/dist/features/shipments/components/AdminShipmentEditorView.js +2 -2
- package/dist/features/shipments/schemas/validation.d.ts +14 -4
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +23 -2
- package/dist/features/tester/components/TesterChecklistStepRow.js +1 -1
- package/dist/features/tester/components/TesterHubView.js +3 -2
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +9 -1
- package/dist/features/tester/seed-data/products-tester-seed-data.js +43 -32
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +404 -15
- package/dist/index.d.ts +15 -3
- package/dist/index.js +22 -5
- package/dist/next/routing/route-map.d.ts +11 -0
- package/dist/next/routing/route-map.js +5 -0
- package/dist/providers/email-resend/provider.js +1 -0
- package/dist/providers/firebase-client/auth.d.ts +1 -0
- package/dist/providers/firebase-client/auth.js +7 -1
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +61 -25
- package/dist/security/pii-mask.js +1 -1
- package/dist/seed/categories-seed-data.js +69 -0
- package/dist/seed/products-auctions-seed-data.js +11 -0
- package/dist/seed/products-standard-seed-data.js +93 -4
- package/dist/seed/site-settings-seed-data.js +13 -1
- package/dist/seed/store-extensions-seed-data.js +1 -1
- package/dist/server-entry.d.ts +2 -1
- package/dist/server-entry.js +20 -2
- package/dist/server.d.ts +7 -2
- package/dist/server.js +8 -3
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Form.d.ts +11 -2
- package/dist/ui/components/Form.js +4 -3
- package/dist/ui/components/FormField.d.ts +3 -1
- package/dist/ui/components/FormField.js +22 -4
- package/dist/ui/components/ListingLayout.d.ts +1 -2
- package/dist/ui/components/ListingLayout.js +1 -1
- package/dist/ui/components/RecordDetailModal.d.ts +68 -0
- package/dist/ui/components/RecordDetailModal.js +8 -0
- package/dist/ui/components/SlottedListingView.js +9 -1
- package/dist/ui/forms/FormErrorSummary.d.ts +25 -0
- package/dist/ui/forms/FormErrorSummary.js +37 -0
- package/dist/ui/forms/FormShell.d.ts +37 -2
- package/dist/ui/forms/FormShell.js +37 -7
- package/dist/ui/forms/FormShell.style.css +17 -0
- package/dist/ui/forms/index.d.ts +2 -0
- package/dist/ui/forms/index.js +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +5 -0
- package/dist/utils/media-url.d.ts +3 -1
- package/dist/utils/media-url.js +20 -1
- package/dist/validation/schemas.d.ts +4 -4
- package/package.json +1 -1
- package/dist/_internal/client/features/classified/ClassifiedDetailView.d.ts +0 -9
- package/dist/_internal/client/features/classified/ClassifiedDetailView.js +0 -55
- package/dist/_internal/client/features/digital-code/DigitalCodeDetailView.d.ts +0 -18
- package/dist/_internal/client/features/digital-code/DigitalCodeDetailView.js +0 -21
- package/dist/_internal/client/features/live/LiveItemDetailView.d.ts +0 -9
- package/dist/_internal/client/features/live/LiveItemDetailView.js +0 -25
- package/dist/_internal/server/jobs/core/prizeRevealClose.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealClose.js +0 -28
- package/dist/_internal/server/jobs/core/prizeRevealExpiry.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +0 -61
- package/dist/_internal/server/jobs/core/prizeRevealOpen.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealOpen.js +0 -68
- package/dist/_internal/server/jobs/core/prizeRevealReminder.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealReminder.js +0 -49
- package/dist/_internal/server/jobs/handlers/prizeRevealClose.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealClose.js +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.js +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealOpen.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealOpen.js +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealReminder.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealReminder.js +0 -2
- package/dist/features/auth/consent-otp.d.ts +0 -57
- package/dist/features/auth/consent-otp.js +0 -127
- package/dist/features/cart/components/CheckoutOtpModal.d.ts +0 -15
- package/dist/features/cart/components/CheckoutOtpModal.js +0 -12
- package/dist/features/checkout/actions/checkout-actions.d.ts +0 -31
- package/dist/features/checkout/actions/checkout-actions.js +0 -124
- package/dist/features/classified/components/ClassifiedIndexListing.d.ts +0 -5
- package/dist/features/classified/components/ClassifiedIndexListing.js +0 -123
- package/dist/features/classified/components/ClassifiedListView.d.ts +0 -6
- package/dist/features/classified/components/ClassifiedListView.js +0 -44
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.d.ts +0 -5
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +0 -114
- package/dist/features/digital-codes/components/DigitalCodesListView.d.ts +0 -6
- package/dist/features/digital-codes/components/DigitalCodesListView.js +0 -38
- package/dist/features/live/components/LiveItemsIndexListing.d.ts +0 -5
- package/dist/features/live/components/LiveItemsIndexListing.js +0 -115
- package/dist/features/live/components/LiveItemsListView.d.ts +0 -6
- package/dist/features/live/components/LiveItemsListView.js +0 -44
- package/dist/features/seller/components/SellerTemplatesView.d.ts +0 -12
- package/dist/features/seller/components/SellerTemplatesView.js +0 -276
- package/dist/react/hooks/useVisibleItems.d.ts +0 -37
- package/dist/react/hooks/useVisibleItems.js +0 -59
- package/dist/ui/components/TabStrip.d.ts +0 -34
- package/dist/ui/components/TabStrip.js +0 -56
- package/dist/ui/components/TabStrip.style.css +0 -97
|
@@ -125,8 +125,16 @@ export interface CategoryDocument extends BaseDocument {
|
|
|
125
125
|
brandCountry?: string;
|
|
126
126
|
/** Brand founding year — categoryType==="brand". */
|
|
127
127
|
brandFounded?: number;
|
|
128
|
-
/**
|
|
129
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Short bullet points — "why shop this category/brand." Category and
|
|
130
|
+
* brand detail pages only; renders nothing when absent/empty.
|
|
131
|
+
*/
|
|
132
|
+
highlights?: string[];
|
|
133
|
+
/** A handful of Q&A pairs shown on the category/brand detail page. */
|
|
134
|
+
faqs?: {
|
|
135
|
+
question: string;
|
|
136
|
+
answer: string;
|
|
137
|
+
}[];
|
|
130
138
|
/**
|
|
131
139
|
* "special" — admin-curated bundle; writes reverse `partOfBundleIds`
|
|
132
140
|
* pointers on member products and shows the "Bundled" badge on cards.
|
|
@@ -135,6 +143,15 @@ export interface CategoryDocument extends BaseDocument {
|
|
|
135
143
|
* Does NOT update `partOfBundleIds`; used as a discovery surface only.
|
|
136
144
|
*/
|
|
137
145
|
bundleKind?: "special" | "brand";
|
|
146
|
+
/**
|
|
147
|
+
* Which brand this bundle belongs to, for the brand detail page's bundle
|
|
148
|
+
* tab — works for both `bundleKind:"special"` (static) and `"brand"`
|
|
149
|
+
* (dynamic) bundles. Distinct from `bundleQueryRule.filter.brandSlug`,
|
|
150
|
+
* which is a dynamic-rule query *filter* (only meaningful when
|
|
151
|
+
* `bundleQueryRule.type==="dynamic"`) — this field is the bundle's own
|
|
152
|
+
* brand tag regardless of how its members are resolved.
|
|
153
|
+
*/
|
|
154
|
+
brandSlug?: string;
|
|
138
155
|
/** Discounted bundle price in decimal rupees. */
|
|
139
156
|
bundlePrice?: number;
|
|
140
157
|
/** Rule resolving the bundle's member products — static list or live query. */
|
|
@@ -229,8 +229,19 @@ export declare const categoryFirestoreSchema: z.ZodObject<{
|
|
|
229
229
|
brandWebsite: z.ZodOptional<z.ZodString>;
|
|
230
230
|
brandCountry: z.ZodOptional<z.ZodString>;
|
|
231
231
|
brandFounded: z.ZodOptional<z.ZodNumber>;
|
|
232
|
-
|
|
232
|
+
highlights: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
233
|
+
faqs: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
234
|
+
question: z.ZodString;
|
|
235
|
+
answer: z.ZodString;
|
|
236
|
+
}, "strip", z.ZodTypeAny, {
|
|
237
|
+
question: string;
|
|
238
|
+
answer: string;
|
|
239
|
+
}, {
|
|
240
|
+
question: string;
|
|
241
|
+
answer: string;
|
|
242
|
+
}>, "many">>;
|
|
233
243
|
bundleKind: z.ZodOptional<z.ZodEnum<["special", "brand"]>>;
|
|
244
|
+
brandSlug: z.ZodOptional<z.ZodString>;
|
|
234
245
|
bundlePrice: z.ZodOptional<z.ZodEffects<z.ZodNumber, number, number>>;
|
|
235
246
|
bundleQueryRule: z.ZodOptional<z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
236
247
|
type: z.ZodLiteral<"static">;
|
|
@@ -419,7 +430,12 @@ export declare const categoryFirestoreSchema: z.ZodObject<{
|
|
|
419
430
|
name: string;
|
|
420
431
|
tier: number;
|
|
421
432
|
}[];
|
|
433
|
+
faqs?: {
|
|
434
|
+
question: string;
|
|
435
|
+
answer: string;
|
|
436
|
+
}[] | undefined;
|
|
422
437
|
description?: string | undefined;
|
|
438
|
+
brandSlug?: string | undefined;
|
|
423
439
|
showOnHomepage?: boolean | undefined;
|
|
424
440
|
categoryType?: "brand" | "category" | "sublisting" | "bundle" | undefined;
|
|
425
441
|
bundleKind?: "brand" | "special" | undefined;
|
|
@@ -442,7 +458,7 @@ export declare const categoryFirestoreSchema: z.ZodObject<{
|
|
|
442
458
|
brandWebsite?: string | undefined;
|
|
443
459
|
brandCountry?: string | undefined;
|
|
444
460
|
brandFounded?: number | undefined;
|
|
445
|
-
|
|
461
|
+
highlights?: string[] | undefined;
|
|
446
462
|
bundlePrice?: number | undefined;
|
|
447
463
|
bundleQueryRule?: {
|
|
448
464
|
type: "static";
|
|
@@ -513,7 +529,12 @@ export declare const categoryFirestoreSchema: z.ZodObject<{
|
|
|
513
529
|
name: string;
|
|
514
530
|
tier: number;
|
|
515
531
|
}[];
|
|
532
|
+
faqs?: {
|
|
533
|
+
question: string;
|
|
534
|
+
answer: string;
|
|
535
|
+
}[] | undefined;
|
|
516
536
|
description?: string | undefined;
|
|
537
|
+
brandSlug?: string | undefined;
|
|
517
538
|
showOnHomepage?: boolean | undefined;
|
|
518
539
|
categoryType?: "brand" | "category" | "sublisting" | "bundle" | undefined;
|
|
519
540
|
bundleKind?: "brand" | "special" | undefined;
|
|
@@ -536,7 +557,7 @@ export declare const categoryFirestoreSchema: z.ZodObject<{
|
|
|
536
557
|
brandWebsite?: string | undefined;
|
|
537
558
|
brandCountry?: string | undefined;
|
|
538
559
|
brandFounded?: number | undefined;
|
|
539
|
-
|
|
560
|
+
highlights?: string[] | undefined;
|
|
540
561
|
bundlePrice?: number | undefined;
|
|
541
562
|
bundleQueryRule?: {
|
|
542
563
|
type: "static";
|
|
@@ -76,8 +76,10 @@ export const categoryFirestoreSchema = z.object({
|
|
|
76
76
|
brandWebsite: z.string().optional(),
|
|
77
77
|
brandCountry: z.string().optional(),
|
|
78
78
|
brandFounded: z.number().int().optional(),
|
|
79
|
-
|
|
79
|
+
highlights: z.array(z.string()).optional(),
|
|
80
|
+
faqs: z.array(z.object({ question: z.string(), answer: z.string() })).optional(),
|
|
80
81
|
bundleKind: z.enum(["special", "brand"]).optional(),
|
|
82
|
+
brandSlug: z.string().optional(),
|
|
81
83
|
bundlePrice: rupeesSchema.optional(),
|
|
82
84
|
bundleQueryRule: bundleQueryRuleSchema.optional(),
|
|
83
85
|
bundleStockStatus: z.enum(["in_stock", "out_of_stock"]).optional(),
|
|
@@ -37,6 +37,13 @@ export interface CategoryItem {
|
|
|
37
37
|
brandCountry?: string;
|
|
38
38
|
/** Brand founded year — only set when categoryType==="brand". */
|
|
39
39
|
brandFounded?: number;
|
|
40
|
+
/** Short bullet points — "why shop this category/brand." */
|
|
41
|
+
highlights?: string[];
|
|
42
|
+
/** A handful of Q&A pairs shown on the category/brand detail page. */
|
|
43
|
+
faqs?: {
|
|
44
|
+
question: string;
|
|
45
|
+
answer: string;
|
|
46
|
+
}[];
|
|
40
47
|
name: string;
|
|
41
48
|
slug: string;
|
|
42
49
|
description?: string;
|
|
@@ -6,9 +6,23 @@
|
|
|
6
6
|
* purpose from `checkout-actions.ts`'s third-party shipping consent OTP —
|
|
7
7
|
* same crypto primitives, separate Firestore namespace and copy.
|
|
8
8
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
export type CheckoutValueOtpChannel = "email" | "whatsapp";
|
|
10
|
+
/**
|
|
11
|
+
* Sends a checkout-value OTP either by email (default) or WhatsApp
|
|
12
|
+
* (opt-in — the buyer must explicitly choose it in the UI). WhatsApp phone
|
|
13
|
+
* is always resolved server-side from the selected shipping address —
|
|
14
|
+
* never trust a client-supplied phone number.
|
|
15
|
+
*
|
|
16
|
+
* If the chosen channel (and, for email, its skip-worthy fallback) is
|
|
17
|
+
* currently circuit-broken or unconfigured, the OTP gate is treated as
|
|
18
|
+
* satisfied (`skipped: true`) rather than blocking checkout — the caller
|
|
19
|
+
* must log this, since it is a security-relevant bypass even though
|
|
20
|
+
* user-authorized by the channel outage.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sendCheckoutValueOtp(userId: string, userEmail: string, channel?: CheckoutValueOtpChannel, addressId?: string): Promise<{
|
|
23
|
+
maskedEmail?: string;
|
|
24
|
+
maskedPhone?: string;
|
|
25
|
+
skipped?: boolean;
|
|
12
26
|
}>;
|
|
13
27
|
/** Verifies the 6-digit checkout-value OTP and marks the Firestore record verified. */
|
|
14
28
|
export declare function verifyCheckoutValueOtp(userId: string, code: string): Promise<void>;
|
|
@@ -8,18 +8,94 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { timingSafeEqual } from "crypto";
|
|
10
10
|
import { ValidationError } from "../../../errors";
|
|
11
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
11
12
|
import { serverLogger } from "../../../monitoring";
|
|
12
13
|
import { sendEmail } from "../../contact/email";
|
|
13
14
|
import { getAdminDb } from "../../../providers/db-firebase";
|
|
14
15
|
import { resolveDate } from "../../../utils";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
import { addressesRepository } from "../../addresses/repository/addresses.repository";
|
|
17
|
+
import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
|
|
18
|
+
import { sendWhatsAppBusinessMessage } from "../../whatsapp-bot/helpers/whatsapp";
|
|
19
|
+
import { isChannelHealthy, withChannelRetry } from "../../../_internal/server/notifications/channel-health";
|
|
20
|
+
import { CHECKOUT_VALUE_OTP_EXPIRY_MS, CHECKOUT_VALUE_OTP_MAX_ATTEMPTS, hashOtp, generateOtpCode, checkoutValueOtpRef, enforceCheckoutValueOtpRateLimit, buildCheckoutValueOtpMessage, } from "../../auth/checkout-value-otp";
|
|
21
|
+
function maskEmail(userEmail) {
|
|
22
|
+
const [local, domain] = userEmail.split("@");
|
|
23
|
+
return domain
|
|
24
|
+
? `${local.length <= 2 ? "*".repeat(local.length) : local[0] + "*".repeat(local.length - 2) + local[local.length - 1]}@${domain}`
|
|
25
|
+
: "***";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Sends a checkout-value OTP either by email (default) or WhatsApp
|
|
29
|
+
* (opt-in — the buyer must explicitly choose it in the UI). WhatsApp phone
|
|
30
|
+
* is always resolved server-side from the selected shipping address —
|
|
31
|
+
* never trust a client-supplied phone number.
|
|
32
|
+
*
|
|
33
|
+
* If the chosen channel (and, for email, its skip-worthy fallback) is
|
|
34
|
+
* currently circuit-broken or unconfigured, the OTP gate is treated as
|
|
35
|
+
* satisfied (`skipped: true`) rather than blocking checkout — the caller
|
|
36
|
+
* must log this, since it is a security-relevant bypass even though
|
|
37
|
+
* user-authorized by the channel outage.
|
|
38
|
+
*/
|
|
39
|
+
export async function sendCheckoutValueOtp(userId, userEmail, channel = "email", addressId) {
|
|
18
40
|
const db = getAdminDb();
|
|
19
41
|
await enforceCheckoutValueOtpRateLimit(db, userId);
|
|
20
42
|
const code = generateOtpCode();
|
|
21
43
|
const codeHash = hashOtp(code);
|
|
22
44
|
const expiresAt = new Date(Date.now() + CHECKOUT_VALUE_OTP_EXPIRY_MS);
|
|
45
|
+
const siteName = process.env.NEXT_PUBLIC_SITE_NAME || "LetItRip";
|
|
46
|
+
if (channel === "whatsapp") {
|
|
47
|
+
if (!addressId) {
|
|
48
|
+
throw new ValidationError("Select a delivery address before sending the code via WhatsApp.");
|
|
49
|
+
}
|
|
50
|
+
const settings = await siteSettingsRepository.getSingleton().catch(() => null);
|
|
51
|
+
const creds = settings?.credentials ?? {};
|
|
52
|
+
const whatsappOtpEnabled = settings?.notificationChannels?.whatsapp?.otpEnabled === true;
|
|
53
|
+
const phoneNumberId = creds.whatsappPhoneNumberId?.trim() ?? "";
|
|
54
|
+
const accessToken = creds.whatsappCloudApiToken?.trim() ?? "";
|
|
55
|
+
const credsReady = phoneNumberId && accessToken && !phoneNumberId.includes("PLACEHOLDER") && !accessToken.includes("PLACEHOLDER");
|
|
56
|
+
if (!whatsappOtpEnabled || !credsReady) {
|
|
57
|
+
throw new ValidationError("WhatsApp verification isn't available right now — send the code by email instead.");
|
|
58
|
+
}
|
|
59
|
+
const address = await addressesRepository.findById(addressId);
|
|
60
|
+
const phone = address?.phone?.trim();
|
|
61
|
+
if (!phone) {
|
|
62
|
+
throw new ValidationError("The selected address has no phone number on file — send the code by email instead.");
|
|
63
|
+
}
|
|
64
|
+
if (!(await isChannelHealthy("whatsapp"))) {
|
|
65
|
+
serverLogger.warn("sendCheckoutValueOtp: WhatsApp channel unhealthy — skipping OTP gate", { uid: userId, channel });
|
|
66
|
+
return { skipped: true };
|
|
67
|
+
}
|
|
68
|
+
await checkoutValueOtpRef(db, userId).set({
|
|
69
|
+
codeHash,
|
|
70
|
+
expiresAt,
|
|
71
|
+
attempts: 0,
|
|
72
|
+
verified: false,
|
|
73
|
+
createdAt: new Date(),
|
|
74
|
+
});
|
|
75
|
+
const message = buildCheckoutValueOtpMessage(code, siteName);
|
|
76
|
+
try {
|
|
77
|
+
const sent = await withChannelRetry("whatsapp", async () => {
|
|
78
|
+
const ok = await sendWhatsAppBusinessMessage({ toPhone: phone, message, phoneNumberId, accessToken });
|
|
79
|
+
if (!ok)
|
|
80
|
+
throw new Error("WhatsApp Cloud API returned a non-OK response");
|
|
81
|
+
return ok;
|
|
82
|
+
});
|
|
83
|
+
if (!sent)
|
|
84
|
+
throw new Error("WhatsApp send failed");
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
void normalizeError(err);
|
|
88
|
+
serverLogger.error("Failed to send checkout value OTP via WhatsApp", { uid: userId, err });
|
|
89
|
+
throw new ValidationError("We couldn't send the verification code via WhatsApp — please try again, or send it by email instead.");
|
|
90
|
+
}
|
|
91
|
+
const maskedPhone = phone.length > 4 ? `${"*".repeat(phone.length - 4)}${phone.slice(-4)}` : "***";
|
|
92
|
+
serverLogger.info(`Checkout value OTP sent via WhatsApp: uid=${userId}`);
|
|
93
|
+
return { maskedPhone };
|
|
94
|
+
}
|
|
95
|
+
if (!(await isChannelHealthy("email"))) {
|
|
96
|
+
serverLogger.warn("sendCheckoutValueOtp: email channel unhealthy — skipping OTP gate", { uid: userId, channel });
|
|
97
|
+
return { skipped: true };
|
|
98
|
+
}
|
|
23
99
|
await checkoutValueOtpRef(db, userId).set({
|
|
24
100
|
codeHash,
|
|
25
101
|
expiresAt,
|
|
@@ -27,24 +103,27 @@ export async function sendCheckoutValueOtp(userId, userEmail) {
|
|
|
27
103
|
verified: false,
|
|
28
104
|
createdAt: new Date(),
|
|
29
105
|
});
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
106
|
+
const maskedEmail = maskEmail(userEmail);
|
|
107
|
+
try {
|
|
108
|
+
await withChannelRetry("email", async () => {
|
|
109
|
+
const { error } = await sendEmail({
|
|
110
|
+
to: userEmail,
|
|
111
|
+
subject: `${siteName}: Verify your order`,
|
|
112
|
+
html: `
|
|
113
|
+
<div style="font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px">
|
|
114
|
+
<h2 style="margin-bottom:8px">Verify Your Order</h2>
|
|
115
|
+
<p style="color:#555">Your cart total requires verification before checkout. Enter this code to continue:</p>
|
|
116
|
+
<div style="font-size:36px;font-weight:bold;letter-spacing:8px;text-align:center;margin:24px 0;padding:16px;background:#f3f4f6;border-radius:8px">${code}</div>
|
|
117
|
+
<p style="color:#888;font-size:12px">This code expires in 10 minutes. If you did not request this, please ignore this email.</p>
|
|
118
|
+
</div>
|
|
119
|
+
`,
|
|
120
|
+
});
|
|
121
|
+
if (error)
|
|
122
|
+
throw new Error(String(error));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
void normalizeError(err);
|
|
48
127
|
// Root-caused 2026-08-20: this used to be fire-and-forget — the
|
|
49
128
|
// Firestore OTP doc was written and the caller always got back a
|
|
50
129
|
// success response ("code sent") regardless of whether the email
|
|
@@ -52,7 +131,7 @@ export async function sendCheckoutValueOtp(userId, userEmail) {
|
|
|
52
131
|
// unverified sending domain, outage) silently stranded the buyer
|
|
53
132
|
// with a checkout they could never complete, since the OTP gate
|
|
54
133
|
// gave no indication anything had gone wrong.
|
|
55
|
-
serverLogger.error("Failed to send checkout value OTP email", { uid: userId,
|
|
134
|
+
serverLogger.error("Failed to send checkout value OTP email", { uid: userId, err });
|
|
56
135
|
throw new ValidationError("We couldn't send the verification code — please try again in a moment, or contact support if this keeps happening.");
|
|
57
136
|
}
|
|
58
137
|
serverLogger.info(`Checkout value OTP sent: uid=${userId}`);
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
|
-
import { useState, useCallback } from "react";
|
|
4
|
+
import { useEffect, useState, useCallback } from "react";
|
|
5
|
+
import { z } from "zod";
|
|
5
6
|
import { Button, Div, Form, Heading, Input, Span, Stack, Text, Textarea } from "../../../ui";
|
|
7
|
+
import { useFormShellState, FormShellContext, FormErrorSummary } from "../../../ui/forms";
|
|
8
|
+
const contactFormSchema = z.object({
|
|
9
|
+
name: z.string().min(1, "Name is required"),
|
|
10
|
+
email: z.string().email("Enter a valid email address"),
|
|
11
|
+
subject: z.string().min(1, "Subject is required"),
|
|
12
|
+
message: z.string().min(10, "Message must be at least 10 characters"),
|
|
13
|
+
});
|
|
6
14
|
const CLS_FIELD_ERROR = "text-error";
|
|
7
15
|
const CLS_ERROR_BANNER = "rounded-lg bg-error-surface dark:bg-error-surface border border-error dark:border-error px-[var(--appkit-space-4)] py-[var(--appkit-space-3)] text-[length:var(--appkit-text-sm)] text-error dark:text-error";
|
|
8
16
|
const INITIAL_FORM = {
|
|
@@ -17,6 +25,11 @@ export function ContactForm({ onSubmit, labels = {}, className = "", }) {
|
|
|
17
25
|
const [submitted, setSubmitted] = useState(false);
|
|
18
26
|
const [errors, setErrors] = useState({});
|
|
19
27
|
const [submitError, setSubmitError] = useState(null);
|
|
28
|
+
const { shellCtx, validate: validateSchema } = useFormShellState(contactFormSchema);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
validateSchema(form);
|
|
31
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
32
|
+
}, [form, validateSchema]);
|
|
20
33
|
const validate = () => {
|
|
21
34
|
const newErrors = {};
|
|
22
35
|
if (!form.name.trim())
|
|
@@ -56,7 +69,7 @@ export function ContactForm({ onSubmit, labels = {}, className = "", }) {
|
|
|
56
69
|
return (_jsxs(Div, { className: `text-center ${className}`, padding: "y-3xl", children: [_jsx(Div, { textSize: "5xl", className: "mb-4", children: "\u2705" }), _jsx(Heading, { level: 2, className: "mb-2", children: labels.successTitle ?? "Message Sent!" }), _jsx(Text, { variant: "secondary", className: "mb-6", children: labels.successDescription ?? "We'll get back to you shortly." }), _jsx(Button, { type: "button", variant: "outline", onClick: () => setSubmitted(false), children: labels.sendAnotherButton ?? "Send another message" })] }));
|
|
57
70
|
}
|
|
58
71
|
const field = (id, label, placeholder, multiline) => (_jsxs(Stack, { gap: "xs", children: [_jsx("label", { htmlFor: id, className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-text-muted)]", children: label }), multiline ? (_jsx(Textarea, { id: id, value: form[id], onChange: (e) => setForm((f) => ({ ...f, [id]: e.target.value })), placeholder: placeholder, rows: 5, className: "w-full rounded-lg border border-neutral-200 border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] outline-none focus:ring-2 ring-primary/20 transition" })) : (_jsx(Input, { id: id, type: id === "email" ? "email" : "text", value: form[id], onChange: (e) => setForm((f) => ({ ...f, [id]: e.target.value })), placeholder: placeholder, className: "h-10" })), errors[id] && _jsx(Span, { size: "xs", className: CLS_FIELD_ERROR, children: errors[id] })] }));
|
|
59
|
-
return (_jsxs(Form, { onSubmit: handleSubmit, noValidate: true, className: className, spacing: "md", children: [_jsx(Heading, { level: 2, className: "mb-6", children: labels.title ?? "Send us a message" }), submitError && (_jsx(Div, { className: CLS_ERROR_BANNER, children: submitError })), field("name", labels.nameLabel ?? "Your Name", labels.namePlaceholder ?? "Enter your name"), field("email", labels.emailLabel ?? "Email", labels.emailPlaceholder ?? "your@email.com"), field("subject", labels.subjectLabel ?? "Subject", labels.subjectPlaceholder ?? "How can we help?"), field("message", labels.messageLabel ?? "Message", labels.messagePlaceholder ?? "Tell us more…", true), _jsx(Button, { type: "submit", variant: "primary", disabled: isSubmitting, className: "w-full sm:w-auto", children: isSubmitting
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
return (_jsx(FormShellContext.Provider, { value: shellCtx, children: _jsxs(Form, { onSubmit: handleSubmit, noValidate: true, className: className, spacing: "md", children: [_jsx(Heading, { level: 2, className: "mb-6", children: labels.title ?? "Send us a message" }), submitError && (_jsx(Div, { className: CLS_ERROR_BANNER, children: submitError })), field("name", labels.nameLabel ?? "Your Name", labels.namePlaceholder ?? "Enter your name"), field("email", labels.emailLabel ?? "Email", labels.emailPlaceholder ?? "your@email.com"), field("subject", labels.subjectLabel ?? "Subject", labels.subjectPlaceholder ?? "How can we help?"), field("message", labels.messageLabel ?? "Message", labels.messagePlaceholder ?? "Tell us more…", true), _jsx(FormErrorSummary, {}), _jsx(Button, { type: "submit", variant: "primary", disabled: isSubmitting, className: "w-full sm:w-auto", children: isSubmitting
|
|
73
|
+
? (labels.submittingButton ?? "Sending…")
|
|
74
|
+
: (labels.submitButton ?? "Send Message") })] }) }));
|
|
62
75
|
}
|
|
@@ -33,6 +33,9 @@ export async function sendEmail(opts) {
|
|
|
33
33
|
html: typeof opts.html === "string" ? opts.html : "",
|
|
34
34
|
text: typeof opts.text === "string" ? opts.text : undefined,
|
|
35
35
|
from: typeof opts.from === "string" ? opts.from : undefined,
|
|
36
|
+
cc: typeof opts.cc === "string" || Array.isArray(opts.cc)
|
|
37
|
+
? opts.cc
|
|
38
|
+
: undefined,
|
|
36
39
|
replyTo: typeof opts.replyTo === "string" ? opts.replyTo : undefined,
|
|
37
40
|
headers: typeof opts.headers === "object" && opts.headers
|
|
38
41
|
? opts.headers
|
|
@@ -2,14 +2,44 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
4
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
5
|
-
import React from "react";
|
|
5
|
+
import React, { useEffect, useMemo } from "react";
|
|
6
|
+
import { z } from "zod";
|
|
6
7
|
import { useQuery } from "@tanstack/react-query";
|
|
7
8
|
import { Alert, Button, Div, Grid, Heading, Input, Label, RichTextEditor, Row, Select, Stack, StackedViewShell, TagInput, Text, Textarea, Toggle, } from "../../../ui";
|
|
9
|
+
import { FormShellContext, useFormShellState, applyZodIssues, FormErrorSummary } from "../../../ui/forms";
|
|
8
10
|
import { apiClient } from "../../../http";
|
|
9
11
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
12
|
import { StepForm } from "../../shell";
|
|
11
13
|
import { ProductInlineSelect } from "../../seller/components/ProductInlineSelect";
|
|
12
14
|
import { CouponInlineSelect } from "../../seller/components/CouponInlineSelect";
|
|
15
|
+
// Covers the cross-type required fields plus the two conditional rules the
|
|
16
|
+
// "Settings" step already hand-enforces (offer coupon/code, poll min-2
|
|
17
|
+
// options) — mirrored here as Zod so they're also live/summary-visible, not
|
|
18
|
+
// just a submit-time gate. The many other per-type fields (survey/feedback
|
|
19
|
+
// form-builder arrays, raffle/spin config) are deliberately left unvalidated
|
|
20
|
+
// — modeling their full conditional shape correctly would need much deeper
|
|
21
|
+
// per-type domain rules than this pass is scoped for.
|
|
22
|
+
const eventDraftSchema = z.object({
|
|
23
|
+
type: z.string(),
|
|
24
|
+
title: z.string().min(1, "Title is required"),
|
|
25
|
+
startsAt: z.string().min(1, "Start date is required"),
|
|
26
|
+
endsAt: z.string().min(1, "End date is required"),
|
|
27
|
+
couponId: z.string(),
|
|
28
|
+
displayCode: z.string(),
|
|
29
|
+
pollOptions: z.array(z.object({ id: z.string(), label: z.string() })),
|
|
30
|
+
}).passthrough().superRefine((v, ctx) => {
|
|
31
|
+
if (v.type === "offer") {
|
|
32
|
+
if (!v.couponId.trim()) {
|
|
33
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["couponId"], message: "Coupon is required for offer events" });
|
|
34
|
+
}
|
|
35
|
+
if (!v.displayCode.trim()) {
|
|
36
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["displayCode"], message: "Display code is required for offer events" });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (v.type === "poll" && v.pollOptions.filter((o) => o.label.trim()).length < 2) {
|
|
40
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["pollOptions"], message: "Poll events require at least 2 options" });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
13
43
|
const __P = {
|
|
14
44
|
p4: "p-[var(--appkit-space-4)]",
|
|
15
45
|
};
|
|
@@ -202,6 +232,11 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, allowedTypes,
|
|
|
202
232
|
const [raffleWinnerName, setRaffleWinnerName] = React.useState("");
|
|
203
233
|
const [raffleEntryCount, setRaffleEntryCount] = React.useState(null);
|
|
204
234
|
const [raffleGithubUrl, setRaffleGithubUrl] = React.useState("");
|
|
235
|
+
const { shellCtx, setFieldError, validate } = useFormShellState(eventDraftSchema);
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
validate(draft);
|
|
238
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
239
|
+
}, [draft, validate]);
|
|
205
240
|
const update = React.useCallback((partial) => {
|
|
206
241
|
setDraft((prev) => ({ ...prev, ...partial }));
|
|
207
242
|
setSaveMessage(null);
|
|
@@ -295,6 +330,10 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, allowedTypes,
|
|
|
295
330
|
onSaved?.(savedId);
|
|
296
331
|
},
|
|
297
332
|
onError: (error) => {
|
|
333
|
+
const issues = error?.issues;
|
|
334
|
+
if (issues && issues.length > 0) {
|
|
335
|
+
applyZodIssues(issues, setFieldError);
|
|
336
|
+
}
|
|
298
337
|
setSaveMessage(error instanceof Error ? error.message : "Save failed");
|
|
299
338
|
},
|
|
300
339
|
});
|
|
@@ -322,6 +361,7 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, allowedTypes,
|
|
|
322
361
|
const steps = [
|
|
323
362
|
{
|
|
324
363
|
label: "Details",
|
|
364
|
+
fields: ["type", "title", "slug", "description", "startsAt", "endsAt", "tags"],
|
|
325
365
|
validate: (values) => {
|
|
326
366
|
if (!values.title.trim())
|
|
327
367
|
return "Title is required";
|
|
@@ -338,10 +378,12 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, allowedTypes,
|
|
|
338
378
|
},
|
|
339
379
|
{
|
|
340
380
|
label: "Media",
|
|
381
|
+
fields: ["coverImageUrl"],
|
|
341
382
|
render: ({ values, onChange }) => (_jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 3, className: "mb-2", children: "Media" }), _jsx(Input, { label: "Cover Image URL (optional)", value: values.coverImageUrl, onChange: (e) => onChange({ coverImageUrl: e.target.value }), placeholder: "https://\u2026", helperText: "Direct link to the event cover image." })] })),
|
|
342
383
|
},
|
|
343
384
|
{
|
|
344
385
|
label: "Settings",
|
|
386
|
+
fields: ["status", "allowGuestParticipation", "discountPercent", "saleBannerText", "couponId", "displayCode", "offerBannerText", "pollOptions", "resultsVisibility", "allowMultiSelect", "allowComment", "maxEntriesPerUser", "hasLeaderboard", "hasPointSystem", "pointsLabel", "entryReviewRequired", "surveyFields", "anonymous", "feedbackFields"],
|
|
345
387
|
validate: (values) => {
|
|
346
388
|
if (values.type === "offer" && !values.couponId.trim())
|
|
347
389
|
return "Coupon ID is required for offer events";
|
|
@@ -355,6 +397,7 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, allowedTypes,
|
|
|
355
397
|
},
|
|
356
398
|
{
|
|
357
399
|
label: "Raffle / Spin",
|
|
400
|
+
fields: ["hasRaffle", "raffleType", "raffleTopN", "rafflePrize", "rafflePrizeCouponId", "rafflePrizeProductIds", "spinPrizes", "spinMaxPerUser", "spinWindowStart", "spinWindowEnd"],
|
|
358
401
|
render: ({ values, onChange }) => {
|
|
359
402
|
const isRaffleType = values.type === "raffle" || values.type === "spin_wheel";
|
|
360
403
|
const showRaffleConfig = isRaffleType || values.hasRaffle;
|
|
@@ -364,7 +407,16 @@ export function AdminEventEditorView({ eventId, onSaved, embedded, allowedTypes,
|
|
|
364
407
|
},
|
|
365
408
|
];
|
|
366
409
|
const alertSection = eventQuery.error ? (_jsx(Alert, { variant: "error", title: "Could not load event", children: eventQuery.error instanceof Error ? eventQuery.error.message : "Unknown error" })) : null;
|
|
367
|
-
const
|
|
410
|
+
const fieldToStepIndex = useMemo(() => {
|
|
411
|
+
const map = {};
|
|
412
|
+
steps.forEach((step, i) => {
|
|
413
|
+
step.fields?.forEach((field) => { map[field] = i; });
|
|
414
|
+
});
|
|
415
|
+
return map;
|
|
416
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
417
|
+
}, [steps.length]);
|
|
418
|
+
const wizardShellCtx = useMemo(() => ({ ...shellCtx, fieldToStepIndex, goToStep: (n) => setCurrentStep(n) }), [shellCtx, fieldToStepIndex, setCurrentStep]);
|
|
419
|
+
const formContent = (_jsxs(Stack, { gap: "sm", children: [alertSection, _jsxs(FormShellContext.Provider, { value: wizardShellCtx, children: [_jsx(FormErrorSummary, {}), _jsx(StepForm, { steps: steps, values: draft, onChange: update, onComplete: () => { saveMutation.mutate(); }, formId: "admin-event", currentStep: currentStep, onStepChange: setCurrentStep, completeLabel: isEdit ? "Save Changes" : "Create Event", isLoading: isLoading })] }), saveMessage && (_jsx(Alert, { variant: saveMessage.toLowerCase().includes("fail") || saveMessage.toLowerCase().includes("error") ? "error" : "success", title: "Save status", children: saveMessage }))] }));
|
|
368
420
|
if (embedded) {
|
|
369
421
|
return _jsx(Div, { className: `${__O.yAuto} ${__P.p4}`, children: formContent });
|
|
370
422
|
}
|
|
@@ -1,22 +1,8 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
|
|
3
|
-
export interface AdminEventEntriesViewProps extends Omit<StackedViewShellProps, "sections"> {
|
|
2
|
+
export interface AdminEventEntriesViewProps {
|
|
4
3
|
eventId?: string;
|
|
5
4
|
entriesEndpoint?: (eventId: string) => string;
|
|
6
5
|
entryReviewEndpoint?: (eventId: string, entryId: string) => string;
|
|
7
6
|
statsEndpoint?: (eventId: string) => string;
|
|
8
|
-
children?: React.ReactNode;
|
|
9
|
-
/** Back-link and event title header */
|
|
10
|
-
renderHeader?: () => React.ReactNode;
|
|
11
|
-
/** Stats banner (total / approved / flagged counts) */
|
|
12
|
-
renderStats?: () => React.ReactNode;
|
|
13
|
-
/** Filter bar (status dropdown, sort, search) */
|
|
14
|
-
renderFilters?: () => React.ReactNode;
|
|
15
|
-
/** Entries data table */
|
|
16
|
-
renderTable?: () => React.ReactNode;
|
|
17
|
-
/** Pagination footer */
|
|
18
|
-
renderPagination?: () => React.ReactNode;
|
|
19
|
-
/** Entry review side-drawer */
|
|
20
|
-
renderReviewDrawer?: () => React.ReactNode;
|
|
21
7
|
}
|
|
22
|
-
export declare function AdminEventEntriesView({ eventId, entriesEndpoint, entryReviewEndpoint, statsEndpoint,
|
|
8
|
+
export declare function AdminEventEntriesView({ eventId, entriesEndpoint, entryReviewEndpoint, statsEndpoint, }: AdminEventEntriesViewProps): React.JSX.Element;
|