@mohasinac/appkit 2.7.26 → 2.7.30
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/classified/ClassifiedDetailView.js +2 -2
- package/dist/_internal/client/features/digital-code/CodeRevealPanel.js +2 -2
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +1 -1
- package/dist/_internal/server/features/checkout/actions.js +42 -35
- package/dist/_internal/server/features/classified/adapters.d.ts +21 -0
- package/dist/_internal/server/features/classified/adapters.js +24 -0
- package/dist/_internal/server/features/classified/index.d.ts +3 -0
- package/dist/_internal/server/features/classified/index.js +3 -0
- package/dist/_internal/server/features/classified/metadata.d.ts +8 -0
- package/dist/_internal/server/features/classified/metadata.js +43 -0
- package/dist/_internal/server/features/classified/og.d.ts +30 -0
- package/dist/_internal/server/features/classified/og.js +76 -0
- package/dist/_internal/server/features/digital-code/adapters.d.ts +21 -0
- package/dist/_internal/server/features/digital-code/adapters.js +29 -0
- package/dist/_internal/server/features/digital-code/index.d.ts +3 -0
- package/dist/_internal/server/features/digital-code/index.js +3 -0
- package/dist/_internal/server/features/digital-code/metadata.d.ts +8 -0
- package/dist/_internal/server/features/digital-code/metadata.js +36 -0
- package/dist/_internal/server/features/digital-code/og.d.ts +27 -0
- package/dist/_internal/server/features/digital-code/og.js +77 -0
- package/dist/_internal/server/features/live/adapters.d.ts +21 -0
- package/dist/_internal/server/features/live/adapters.js +34 -0
- package/dist/_internal/server/features/live/index.d.ts +3 -0
- package/dist/_internal/server/features/live/index.js +3 -0
- package/dist/_internal/server/features/live/metadata.d.ts +8 -0
- package/dist/_internal/server/features/live/metadata.js +39 -0
- package/dist/_internal/server/features/live/og.d.ts +27 -0
- package/dist/_internal/server/features/live/og.js +72 -0
- package/dist/_internal/server/features/promotions/actions.js +4 -3
- package/dist/_internal/server/jobs/core/onOrderCreate.js +19 -16
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +25 -22
- package/dist/_internal/server/jobs/core/onProductWrite.js +48 -58
- package/dist/_internal/shared/actions/action-registry.js +310 -8
- package/dist/_internal/shared/features/blog/schema.d.ts +4 -4
- package/dist/_internal/shared/features/brands/schema.d.ts +5 -5
- package/dist/_internal/shared/features/cart/schema.d.ts +18 -18
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +1 -1
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +38 -38
- package/dist/_internal/shared/features/events/schema.d.ts +8 -8
- package/dist/_internal/shared/features/orders/schema.d.ts +8 -8
- package/dist/_internal/shared/features/products/schema.d.ts +42 -42
- package/dist/_internal/shared/features/promotions/schema.d.ts +4 -4
- package/dist/_internal/shared/features/reviews/schema.d.ts +1 -1
- package/dist/_internal/shared/media/limits.js +4 -3
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/configs/next.js +4 -0
- package/dist/constants/table-keys.d.ts +6 -0
- package/dist/constants/table-keys.js +9 -0
- package/dist/core/hooks/useSyncManager.js +9 -8
- package/dist/features/about/components/FeesView.js +10 -7
- package/dist/features/about/components/HelpPageView.js +10 -1
- package/dist/features/about/components/HowAuctionsWorkView.js +10 -1
- package/dist/features/about/components/HowCheckoutWorksView.js +10 -1
- package/dist/features/about/components/HowOffersWorkView.js +10 -1
- package/dist/features/about/components/HowOrdersWorkView.js +10 -1
- package/dist/features/about/components/HowPayoutsWorkView.js +10 -1
- package/dist/features/about/components/HowPreOrdersWorkView.js +10 -1
- package/dist/features/about/components/HowReviewsWorkView.js +10 -1
- package/dist/features/about/components/PublicProfileView.js +44 -14
- package/dist/features/about/components/SecurityPrivacyView.js +7 -1
- package/dist/features/account/components/AddressesIndexListing.js +2 -2
- package/dist/features/account/components/NotificationBell.js +27 -16
- package/dist/features/account/components/NotificationPreferencesPanel.d.ts +9 -0
- package/dist/features/account/components/NotificationPreferencesPanel.js +91 -0
- package/dist/features/account/components/UserSidebar.js +4 -4
- package/dist/features/account/components/UserSupportView.js +16 -6
- package/dist/features/account/components/index.d.ts +2 -0
- package/dist/features/account/components/index.js +1 -0
- package/dist/features/account/schemas/index.d.ts +4 -4
- package/dist/features/account/types/index.d.ts +34 -0
- package/dist/features/admin/actions/admin-actions.js +4 -3
- package/dist/features/admin/actions/notification-actions.d.ts +24 -22
- package/dist/features/admin/actions/notification-actions.js +122 -22
- package/dist/features/admin/components/AdminAdEditorView.js +29 -15
- package/dist/features/admin/components/AdminAdsView.js +22 -28
- package/dist/features/admin/components/AdminAllEventEntriesView.js +6 -1
- package/dist/features/admin/components/AdminBidsView.js +10 -3
- package/dist/features/admin/components/AdminBlogEditorView.js +2 -2
- package/dist/features/admin/components/AdminBlogView.js +8 -3
- package/dist/features/admin/components/AdminBrandsView.js +7 -2
- package/dist/features/admin/components/AdminBundleEditorView.js +5 -2
- package/dist/features/admin/components/AdminCarouselEditorView.js +6 -3
- package/dist/features/admin/components/AdminCarouselView.js +13 -7
- package/dist/features/admin/components/AdminCategoriesView.js +8 -3
- package/dist/features/admin/components/AdminCategoryEditorView.js +2 -2
- package/dist/features/admin/components/AdminContactView.js +6 -1
- package/dist/features/admin/components/AdminCouponEditorView.js +8 -2
- package/dist/features/admin/components/AdminCouponsView.js +6 -1
- package/dist/features/admin/components/AdminEmployeeEditorView.js +12 -9
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -2
- package/dist/features/admin/components/AdminFaqsView.js +7 -2
- package/dist/features/admin/components/AdminFeaturesView.js +4 -3
- package/dist/features/admin/components/AdminMediaView.js +9 -8
- package/dist/features/admin/components/AdminNavigationView.js +10 -17
- package/dist/features/admin/components/AdminNewsletterView.js +6 -1
- package/dist/features/admin/components/AdminNotificationsView.js +11 -3
- package/dist/features/admin/components/AdminOrdersView.js +6 -1
- package/dist/features/admin/components/AdminPayoutsView.js +10 -3
- package/dist/features/admin/components/AdminPrizeDrawsView.js +10 -3
- package/dist/features/admin/components/AdminProductsView.js +18 -12
- package/dist/features/admin/components/AdminReviewsView.js +33 -24
- package/dist/features/admin/components/AdminScammerEditorView.js +3 -2
- package/dist/features/admin/components/AdminScammersView.js +28 -26
- package/dist/features/admin/components/AdminSectionsView.js +112 -99
- package/dist/features/admin/components/AdminSessionsView.js +10 -3
- package/dist/features/admin/components/AdminSidebar.js +4 -4
- package/dist/features/admin/components/AdminSiteSettingsView.js +45 -4
- package/dist/features/admin/components/AdminStoresView.js +6 -1
- package/dist/features/admin/components/AdminSublistingCategoriesView.js +0 -1
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +1 -1
- package/dist/features/admin/components/AdminSupportTicketsView.js +32 -34
- package/dist/features/admin/components/AdminTeamView.js +6 -1
- package/dist/features/admin/components/AdminUserEditorView.js +8 -15
- package/dist/features/admin/components/AdminUsersView.js +6 -1
- package/dist/features/admin/components/AdminViewCards.js +4 -3
- package/dist/features/admin/components/DashboardStats.js +2 -2
- package/dist/features/admin/components/DataTable.js +2 -2
- package/dist/features/admin/components/sections/adminSectionsTypes.js +4 -3
- package/dist/features/admin/repository/chat.repository.js +8 -8
- package/dist/features/admin/repository/site-settings.repository.d.ts +1 -0
- package/dist/features/admin/repository/site-settings.repository.js +11 -9
- package/dist/features/admin/schemas/firestore.d.ts +38 -0
- package/dist/features/admin/schemas/firestore.js +15 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +13 -19
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +18 -9
- package/dist/features/auctions/components/MarketplaceAuctionGrid.js +3 -2
- package/dist/features/auctions/components/index.d.ts +0 -1
- package/dist/features/auctions/components/index.js +0 -1
- package/dist/features/auctions/schemas/index.d.ts +14 -14
- package/dist/features/auth/actions/realtime-token-actions.js +19 -4
- package/dist/features/auth/components/RegisterForm.js +8 -4
- package/dist/features/auth/consent-otp.d.ts +2 -0
- package/dist/features/auth/consent-otp.js +8 -0
- package/dist/features/auth/schemas/firestore.d.ts +2 -0
- package/dist/features/auth/schemas/firestore.js +18 -25
- package/dist/features/auth/schemas/index.d.ts +2 -2
- package/dist/features/before-after/api/[id]/route.js +8 -6
- package/dist/features/before-after/schemas/index.d.ts +2 -2
- package/dist/features/blog/actions/blog-actions.d.ts +3 -3
- package/dist/features/blog/components/BlogIndexListing.js +21 -2
- package/dist/features/blog/components/BlogPostForm.js +11 -5
- package/dist/features/blog/components/BlogPostView.js +10 -2
- package/dist/features/blog/schemas/index.d.ts +18 -18
- package/dist/features/cart/components/CartDrawer.js +3 -2
- package/dist/features/cart/repository/cart.repository.js +4 -3
- package/dist/features/cart/utils/pending-ops.d.ts +1 -1
- package/dist/features/categories/api/[id]/route.js +4 -3
- package/dist/features/categories/api/route.js +4 -3
- package/dist/features/categories/components/BrandDetailPageView.js +2 -2
- package/dist/features/categories/components/BrandDetailTabs.js +2 -2
- package/dist/features/categories/components/BundleCollage.d.ts +8 -0
- package/dist/features/categories/components/BundleCollage.js +50 -0
- package/dist/features/categories/components/BundleDetailView.js +2 -10
- package/dist/features/categories/components/BundleItemsPicker.js +20 -13
- package/dist/features/categories/components/CategoriesIndexListing.js +28 -9
- package/dist/features/categories/components/CategoryDetailPageView.js +2 -2
- package/dist/features/categories/components/CategoryDetailTabs.js +2 -2
- package/dist/features/categories/components/CategoryProductsListing.js +52 -13
- package/dist/features/categories/components/index.d.ts +2 -0
- package/dist/features/categories/components/index.js +1 -0
- package/dist/features/categories/schemas/index.d.ts +6 -6
- package/dist/features/checkout/actions/checkout-actions.js +4 -3
- package/dist/features/checkout/hooks/usePaymentCheckout.js +31 -22
- package/dist/features/classified/components/ClassifiedFilters.d.ts +8 -0
- package/dist/features/classified/components/ClassifiedFilters.js +19 -0
- package/dist/features/classified/components/ClassifiedIndexListing.d.ts +4 -0
- package/dist/features/classified/components/ClassifiedIndexListing.js +110 -0
- package/dist/features/classified/components/ClassifiedListView.d.ts +6 -0
- package/dist/features/classified/components/ClassifiedListView.js +43 -0
- package/dist/features/classified/components/index.d.ts +6 -0
- package/dist/features/classified/components/index.js +3 -0
- package/dist/features/collections/api/[slug]/route.js +16 -12
- package/dist/features/collections/schemas/index.d.ts +5 -5
- package/dist/features/consultation/api/[id]/route.js +12 -9
- package/dist/features/consultation/components/ConsultationForm.js +3 -2
- package/dist/features/consultation/schemas/index.d.ts +6 -6
- package/dist/features/contact/email.js +77 -116
- package/dist/features/copilot/components/AdminCopilotView.js +12 -2
- package/dist/features/corporate/api/[id]/route.js +12 -9
- package/dist/features/corporate/components/CorporateInquiryForm.js +2 -1
- package/dist/features/digital-codes/components/DigitalCodeFilters.d.ts +8 -0
- package/dist/features/digital-codes/components/DigitalCodeFilters.js +16 -0
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.d.ts +4 -0
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +107 -0
- package/dist/features/digital-codes/components/DigitalCodesListView.d.ts +6 -0
- package/dist/features/digital-codes/components/DigitalCodesListView.js +37 -0
- package/dist/features/digital-codes/components/index.d.ts +6 -0
- package/dist/features/digital-codes/components/index.js +3 -0
- package/dist/features/events/actions/event-actions.js +4 -3
- package/dist/features/events/components/AdminEventEditorView.js +3 -2
- package/dist/features/events/components/AdminEventsView.js +4 -2
- package/dist/features/events/components/EventPollWidget.js +5 -5
- package/dist/features/events/components/EventsIndexListing.js +2 -2
- package/dist/features/events/schemas/index.d.ts +20 -20
- package/dist/features/faq/actions/faq-actions.d.ts +8 -8
- package/dist/features/faq/components/ContactCTA.js +3 -1
- package/dist/features/faq/components/FAQAccordion.js +1 -1
- package/dist/features/faq/schemas/index.d.ts +4 -4
- package/dist/features/filters/AsyncFacetSection.d.ts +18 -0
- package/dist/features/filters/AsyncFacetSection.js +65 -0
- package/dist/features/forms/Checkbox.js +2 -2
- package/dist/features/forms/Input.js +2 -2
- package/dist/features/forms/Radio.js +5 -5
- package/dist/features/forms/Select.js +2 -2
- package/dist/features/forms/Slider.js +2 -2
- package/dist/features/forms/Textarea.js +2 -2
- package/dist/features/homepage/api/[id]/route.js +4 -3
- package/dist/features/homepage/api/sections/[id]/route.js +12 -9
- package/dist/features/homepage/components/AdSlot.js +2 -1
- package/dist/features/homepage/components/AdvertisementBanner.js +5 -3
- package/dist/features/homepage/components/BrandsSection.js +2 -2
- package/dist/features/homepage/components/CTABannerSection.js +2 -2
- package/dist/features/homepage/components/CharacterHotspot.d.ts +2 -2
- package/dist/features/homepage/components/CharacterHotspot.js +119 -157
- package/dist/features/homepage/components/CharacterHotspotForm.js +25 -20
- package/dist/features/homepage/components/CustomCardsSection.js +8 -6
- package/dist/features/homepage/components/FeaturedProductsSection.js +2 -2
- package/dist/features/homepage/components/GoogleReviewsSection.js +3 -3
- package/dist/features/homepage/components/HeroBanner.js +5 -4
- package/dist/features/homepage/components/HeroCarousel.js +13 -26
- package/dist/features/homepage/components/HowItWorksSection.js +4 -2
- package/dist/features/homepage/components/SocialFeedSection.js +2 -2
- package/dist/features/homepage/components/SocialPostCard.js +3 -2
- package/dist/features/homepage/lib/social-feed-fetcher.js +6 -3
- package/dist/features/layout/AppLayoutShell.d.ts +8 -2
- package/dist/features/layout/AppLayoutShell.js +55 -84
- package/dist/features/layout/BottomActions.js +43 -48
- package/dist/features/layout/NavbarWithSettings.d.ts +6 -0
- package/dist/features/layout/NavbarWithSettings.js +11 -0
- package/dist/features/live/components/LiveItemFilters.d.ts +8 -0
- package/dist/features/live/components/LiveItemFilters.js +23 -0
- package/dist/features/live/components/LiveItemsIndexListing.d.ts +4 -0
- package/dist/features/live/components/LiveItemsIndexListing.js +108 -0
- package/dist/features/live/components/LiveItemsListView.d.ts +6 -0
- package/dist/features/live/components/LiveItemsListView.js +43 -0
- package/dist/features/live/components/index.d.ts +6 -0
- package/dist/features/live/components/index.js +3 -0
- package/dist/features/media/MediaLightbox.js +8 -14
- package/dist/features/media/types/index.d.ts +2 -2
- package/dist/features/media/upload/CameraCapture.js +2 -2
- package/dist/features/media/upload/ImageUpload.js +4 -2
- package/dist/features/media/upload/MediaUploadField.js +24 -8
- package/dist/features/media/upload/MediaUploadList.js +1 -1
- package/dist/features/orders/actions/order-actions.js +4 -3
- package/dist/features/orders/components/MarketplaceOrderCard.js +2 -1
- package/dist/features/orders/components/OrderFilters.js +12 -9
- package/dist/features/orders/schemas/index.d.ts +6 -6
- package/dist/features/pre-orders/api/[id]/route.js +12 -9
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +5 -4
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +27 -20
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +17 -52
- package/dist/features/pre-orders/components/PreorderCard.d.ts +0 -9
- package/dist/features/pre-orders/components/PreorderCard.js +2 -13
- package/dist/features/pre-orders/components/index.d.ts +1 -1
- package/dist/features/pre-orders/components/index.js +1 -1
- package/dist/features/pre-orders/schemas/index.d.ts +10 -10
- package/dist/features/products/api/[id]/route.js +9 -7
- package/dist/features/products/api/route.js +1 -1
- package/dist/features/products/columns/productTableColumns.js +2 -2
- package/dist/features/products/components/AuctionsIndexListing.js +15 -51
- package/dist/features/products/components/BidHistory.js +2 -2
- package/dist/features/products/components/CompareOverlay.js +3 -2
- package/dist/features/products/components/MakeOfferButton.js +20 -14
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +2 -1
- package/dist/features/products/components/PrizeDrawCollage.d.ts +1 -15
- package/dist/features/products/components/PrizeDrawCollage.js +54 -14
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/PrizeDrawItemsEditor.js +9 -7
- package/dist/features/products/components/PrizeDrawsIndexListing.js +18 -51
- package/dist/features/products/components/PrizeRevealModal.js +24 -14
- package/dist/features/products/components/ProductDetailPageView.js +2 -1
- package/dist/features/products/components/ProductFilters.js +8 -6
- package/dist/features/products/components/ProductForm.js +12 -10
- package/dist/features/products/components/ProductGalleryClient.js +2 -1
- package/dist/features/products/components/ProductGrid.js +11 -14
- package/dist/features/products/components/ProductsIndexListing.js +16 -52
- package/dist/features/products/components/ProductsIndexPageView.js +2 -2
- package/dist/features/products/schemas/index.d.ts +56 -56
- package/dist/features/products/schemas/product-features.validators.d.ts +14 -14
- package/dist/features/promotions/components/CouponsIndexListing.js +1 -1
- package/dist/features/promotions/schemas/index.d.ts +6 -6
- package/dist/features/reviews/actions/review-actions.js +6 -5
- package/dist/features/reviews/api/[id]/route.js +4 -3
- package/dist/features/reviews/components/ReviewDetailPageView.js +2 -2
- package/dist/features/reviews/components/ReviewDetailShell.js +6 -3
- package/dist/features/reviews/components/ReviewsIndexListing.js +2 -2
- package/dist/features/reviews/schemas/index.d.ts +6 -6
- package/dist/features/scams/components/ScamProfileView.js +19 -10
- package/dist/features/scams/components/ScamRegistryView.js +2 -1
- package/dist/features/search/components/Search.d.ts +1 -1
- package/dist/features/search/components/Search.js +56 -49
- package/dist/features/search/schemas/index.d.ts +5 -5
- package/dist/features/seller/actions/offer-actions.js +11 -9
- package/dist/features/seller/components/SellerAddressesView.js +6 -4
- package/dist/features/seller/components/SellerBidsView.js +2 -2
- package/dist/features/seller/components/SellerCouponsView.js +1 -1
- package/dist/features/seller/components/SellerOffersPanel.js +6 -1
- package/dist/features/seller/components/SellerOrdersView.js +1 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +3 -3
- package/dist/features/seller/components/SellerPreOrdersView.js +5 -4
- package/dist/features/seller/components/SellerPrizeDrawsView.js +5 -4
- package/dist/features/seller/components/SellerProductShell.d.ts +19 -1
- package/dist/features/seller/components/SellerProductShell.js +75 -9
- package/dist/features/seller/components/SellerProductsView.js +10 -11
- package/dist/features/seller/components/SellerShippingView.js +3 -3
- package/dist/features/seller/components/SellerSidebar.js +10 -7
- package/dist/features/seller/components/SellerStorefrontView.js +3 -3
- package/dist/features/seller/schemas/index.d.ts +4 -4
- package/dist/features/shell/FormShell.js +2 -1
- package/dist/features/shell/QuickFormDrawer.js +4 -3
- package/dist/features/shell/StepForm.js +4 -3
- package/dist/features/stores/actions/store-query-actions.js +4 -3
- package/dist/features/stores/components/StoreAboutView.js +2 -1
- package/dist/features/stores/components/StoreAuctionsListing.js +43 -64
- package/dist/features/stores/components/StoreDetailLayoutView.js +2 -2
- package/dist/features/stores/components/StorePreOrdersListing.js +33 -57
- package/dist/features/stores/components/StoreProductsListing.js +16 -49
- package/dist/features/stores/components/StoresIndexListing.js +2 -2
- package/dist/features/stores/schemas/index.d.ts +2 -2
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +8 -7
- package/dist/features/wishlist/hooks/useGuestWishlist.d.ts +5 -5
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +6 -6
- package/dist/features/wishlist/utils/guest-wishlist.d.ts +5 -5
- package/dist/index.d.ts +21 -3
- package/dist/index.js +1737 -1725
- package/dist/monitoring/server-logger.js +5 -9
- package/dist/next/api/apiHandler.js +56 -37
- package/dist/next/components/GlobalError.js +2 -1
- package/dist/next/routing/route-map.d.ts +27 -0
- package/dist/next/routing/route-map.js +10 -0
- package/dist/react/ErrorBoundary.js +2 -1
- package/dist/react/contexts/SessionContext.js +38 -28
- package/dist/react/hooks/useCamera.d.ts +1 -1
- package/dist/react/hooks/useCamera.js +8 -7
- package/dist/seed/site-settings-seed-data.js +6 -0
- package/dist/server-entry.d.ts +3 -0
- package/dist/server-entry.js +3 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +15 -0
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/DataTable.js +28 -7
- package/dist/ui/components/Button.d.ts +8 -1
- package/dist/ui/components/Button.js +47 -8
- package/dist/ui/components/CollageLightbox.d.ts +18 -0
- package/dist/ui/components/CollageLightbox.js +41 -0
- package/dist/ui/components/Dropdown.js +7 -6
- package/dist/ui/components/ImageLightbox.d.ts +7 -1
- package/dist/ui/components/ImageLightbox.js +15 -5
- package/dist/ui/components/PageLoader.js +4 -3
- package/dist/ui/components/PaginatedMultiSelect.d.ts +28 -0
- package/dist/ui/components/PaginatedMultiSelect.js +141 -0
- package/dist/ui/components/PaginatedMultiSelect.style.css +272 -0
- package/dist/ui/components/Semantic.js +1 -1
- package/dist/ui/components/SideDrawer.js +17 -15
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/dist/utils/number.formatter.js +5 -13
- package/dist/utils/string.formatter.js +21 -18
- package/dist/validation/schemas.d.ts +2 -2
- package/package.json +2 -2
|
@@ -1,32 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Notification Domain Actions (appkit)
|
|
3
|
-
*
|
|
4
|
-
* Pure business functions — auth, rate-limiting, and Next.js specifics
|
|
5
|
-
* are handled by the calling server action in the consumer.
|
|
6
|
-
*/
|
|
7
1
|
import { notificationRepository } from "../repository/notification.repository";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
2
|
+
import { siteSettingsRepository } from "../repository/site-settings.repository";
|
|
3
|
+
import { userRepository } from "../../auth/repository/user.repository";
|
|
4
|
+
import { createResendProvider } from "../../../providers/email-resend/provider";
|
|
5
|
+
import { sendWhatsAppBusinessMessage } from "../../whatsapp-bot/helpers/whatsapp";
|
|
6
|
+
import { serverLogger } from "../../../monitoring";
|
|
7
|
+
import { DEFAULT_NOTIFICATION_CHANNELS, meetsMinPriority, } from "../schemas/firestore";
|
|
11
8
|
export async function markNotificationRead(id) {
|
|
12
9
|
await notificationRepository.markAsRead(id);
|
|
13
10
|
}
|
|
14
|
-
/**
|
|
15
|
-
* Mark all notifications as read for a given user.
|
|
16
|
-
* Returns the number of notifications updated.
|
|
17
|
-
*/
|
|
18
11
|
export async function markAllNotificationsRead(userId) {
|
|
19
12
|
return notificationRepository.markAllAsRead(userId);
|
|
20
13
|
}
|
|
21
|
-
/**
|
|
22
|
-
* Delete a single notification by id.
|
|
23
|
-
*/
|
|
24
14
|
export async function deleteNotification(id) {
|
|
25
15
|
await notificationRepository.delete(id);
|
|
26
16
|
}
|
|
27
|
-
/**
|
|
28
|
-
* List notifications for a user with unread count.
|
|
29
|
-
*/
|
|
30
17
|
export async function listNotifications(userId, limit = 20) {
|
|
31
18
|
const [notifications, unreadCount] = await Promise.all([
|
|
32
19
|
notificationRepository.findByUser(userId, limit),
|
|
@@ -34,9 +21,122 @@ export async function listNotifications(userId, limit = 20) {
|
|
|
34
21
|
]);
|
|
35
22
|
return { notifications, unreadCount };
|
|
36
23
|
}
|
|
37
|
-
/**
|
|
38
|
-
* Get the unread notification count for a user.
|
|
39
|
-
*/
|
|
40
24
|
export async function getUnreadNotificationCount(userId) {
|
|
41
25
|
return notificationRepository.getUnreadCount(userId);
|
|
42
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Create an in-app notification and fan out to enabled external channels.
|
|
29
|
+
*
|
|
30
|
+
* In-app is always written. Email / WhatsApp are sent only when:
|
|
31
|
+
* 1. The channel is enabled in siteSettings.notificationChannels
|
|
32
|
+
* 2. Notification priority meets the channel's minPriority threshold
|
|
33
|
+
* 3. The optional `types` filter includes this notification type (or is absent)
|
|
34
|
+
* 4. The contact field (userEmail / userPhone) is supplied by the caller
|
|
35
|
+
* 5. The required credentials are set in siteSettings.credentials
|
|
36
|
+
*/
|
|
37
|
+
export async function sendNotification(input) {
|
|
38
|
+
const { userEmail, userPhone, emailHtml, ...notifInput } = input;
|
|
39
|
+
// Always write the in-app notification first.
|
|
40
|
+
const notification = await notificationRepository.create(notifInput);
|
|
41
|
+
// Load channel config + credentials (one Firestore read).
|
|
42
|
+
let settings;
|
|
43
|
+
try {
|
|
44
|
+
settings = await siteSettingsRepository.getSingleton();
|
|
45
|
+
}
|
|
46
|
+
catch (err) {
|
|
47
|
+
serverLogger.warn("sendNotification: could not load siteSettings — skipping external channels", { err });
|
|
48
|
+
return { notification, email: "skipped", whatsapp: "skipped" };
|
|
49
|
+
}
|
|
50
|
+
const channels = settings.notificationChannels ?? DEFAULT_NOTIFICATION_CHANNELS;
|
|
51
|
+
const creds = settings.credentials ?? {};
|
|
52
|
+
const { type, priority, title, message } = notifInput;
|
|
53
|
+
// Load user notification preferences (best-effort — if missing, all channels allowed).
|
|
54
|
+
let userChannelPrefs = {};
|
|
55
|
+
let userTypePrefs = {};
|
|
56
|
+
try {
|
|
57
|
+
const userDoc = await userRepository.findById(input.userId);
|
|
58
|
+
userChannelPrefs = userDoc?.notificationPreferences?.channels ?? {};
|
|
59
|
+
userTypePrefs = userDoc?.notificationPreferences?.types ?? {};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// non-fatal: fall through with empty prefs (all enabled)
|
|
63
|
+
}
|
|
64
|
+
// Map NotificationType to a NotificationTypePrefs key.
|
|
65
|
+
const typeToPrefsKey = {
|
|
66
|
+
order_placed: "orderUpdates", order_confirmed: "orderUpdates",
|
|
67
|
+
order_shipped: "orderUpdates", order_delivered: "orderUpdates",
|
|
68
|
+
order_cancelled: "orderUpdates",
|
|
69
|
+
bid_placed: "bids", bid_outbid: "bids", bid_won: "bids", bid_lost: "bids",
|
|
70
|
+
review_approved: "reviews", review_replied: "reviews",
|
|
71
|
+
promotion: "promotions",
|
|
72
|
+
system: "system", welcome: "system",
|
|
73
|
+
offer_received: "offers", offer_responded: "offers",
|
|
74
|
+
offer_expired: "offers", offer_counter_accepted: "offers",
|
|
75
|
+
refund_initiated: "orderUpdates",
|
|
76
|
+
product_available: "system",
|
|
77
|
+
};
|
|
78
|
+
const typeKey = typeToPrefsKey[type];
|
|
79
|
+
// If the user has explicitly disabled this notification type, skip all external channels.
|
|
80
|
+
if (typeKey && userTypePrefs[typeKey] === false) {
|
|
81
|
+
return { notification, email: "skipped", whatsapp: "skipped" };
|
|
82
|
+
}
|
|
83
|
+
// Email channel — admin enabled AND user hasn't opted out
|
|
84
|
+
let emailStatus = "skipped";
|
|
85
|
+
if (channels.email.enabled &&
|
|
86
|
+
userChannelPrefs.email !== false &&
|
|
87
|
+
userEmail &&
|
|
88
|
+
meetsMinPriority(priority, channels.email.minPriority) &&
|
|
89
|
+
(!channels.email.types?.length || channels.email.types.includes(type))) {
|
|
90
|
+
const apiKey = creds.resendApiKey?.trim() ?? "";
|
|
91
|
+
const fromEmail = settings.emailSettings?.fromEmail ?? "noreply@letitrip.in";
|
|
92
|
+
const fromName = settings.emailSettings?.fromName ?? "LetItRip";
|
|
93
|
+
if (apiKey && !apiKey.includes("PLACEHOLDER")) {
|
|
94
|
+
try {
|
|
95
|
+
const emailProvider = createResendProvider({ apiKey, fromEmail, fromName });
|
|
96
|
+
await emailProvider.send({
|
|
97
|
+
to: userEmail,
|
|
98
|
+
subject: title,
|
|
99
|
+
html: emailHtml ?? `<p>${message}</p>`,
|
|
100
|
+
text: message,
|
|
101
|
+
});
|
|
102
|
+
emailStatus = "sent";
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
serverLogger.error("sendNotification: email dispatch failed", { userId: input.userId, err });
|
|
106
|
+
emailStatus = "failed";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// WhatsApp channel — admin enabled AND user hasn't opted out
|
|
111
|
+
let whatsappStatus = "skipped";
|
|
112
|
+
if (channels.whatsapp.enabled &&
|
|
113
|
+
userChannelPrefs.whatsapp !== false &&
|
|
114
|
+
userPhone &&
|
|
115
|
+
meetsMinPriority(priority, channels.whatsapp.minPriority) &&
|
|
116
|
+
(!channels.whatsapp.types?.length || channels.whatsapp.types.includes(type))) {
|
|
117
|
+
const phoneNumberId = creds.whatsappPhoneNumberId?.trim() ?? "";
|
|
118
|
+
const accessToken = creds.whatsappCloudApiToken?.trim() ?? "";
|
|
119
|
+
if (phoneNumberId && accessToken) {
|
|
120
|
+
try {
|
|
121
|
+
const ok = await sendWhatsAppBusinessMessage({
|
|
122
|
+
toPhone: userPhone,
|
|
123
|
+
message: `*${title}*\n${message}`,
|
|
124
|
+
phoneNumberId,
|
|
125
|
+
accessToken,
|
|
126
|
+
});
|
|
127
|
+
whatsappStatus = ok ? "sent" : "failed";
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
serverLogger.error("sendNotification: WhatsApp dispatch failed", { userId: input.userId, err });
|
|
131
|
+
whatsappStatus = "failed";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
serverLogger.info("sendNotification: dispatched", {
|
|
136
|
+
userId: input.userId,
|
|
137
|
+
type,
|
|
138
|
+
email: emailStatus,
|
|
139
|
+
whatsapp: whatsappStatus,
|
|
140
|
+
});
|
|
141
|
+
return { notification, email: emailStatus, whatsapp: whatsappStatus };
|
|
142
|
+
}
|
|
@@ -5,6 +5,27 @@ import { useMutation, useQuery } from "@tanstack/react-query";
|
|
|
5
5
|
import { Alert, Button, Form, FormActions, Input, Select, StackedViewShell, Text, Toggle, useToast, } from "../../../ui";
|
|
6
6
|
import { apiClient } from "../../../http";
|
|
7
7
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
|
+
function validateThirdPartyUrl(url, issues) {
|
|
9
|
+
if (!url.trim()) {
|
|
10
|
+
issues.push("Third-party ads require a third-party URL");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const parsed = new URL(url.trim());
|
|
15
|
+
if (parsed.protocol !== "https:") {
|
|
16
|
+
issues.push("Third-party ad URL must be https");
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
issues.push("Third-party ad URL must be a valid URL");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function renderAdScheduleSection({ startAt, setStartAt, endAt, setEndAt }) {
|
|
24
|
+
return (_jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", "data-section": "adminadeditorview-div-238", children: [_jsx(Input, { label: "Start at (ISO)", value: startAt, onChange: (event) => setStartAt(event.target.value), placeholder: "2026-05-01T00:00:00.000Z" }), _jsx(Input, { label: "End at (ISO)", value: endAt, onChange: (event) => setEndAt(event.target.value), placeholder: "2026-05-30T23:59:59.000Z" })] }));
|
|
25
|
+
}
|
|
26
|
+
function renderAdCreativeSection({ title, setTitle, body, setBody, imageUrl, setImageUrl, ctaLabel, setCtaLabel, ctaHref, setCtaHref, adsenseSlot, setAdsenseSlot, thirdPartyUrl, setThirdPartyUrl, provider, }) {
|
|
27
|
+
return (_jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", "data-section": "adminadeditorview-div-239", children: [_jsx(Input, { label: "Creative title", value: title, onChange: (e) => setTitle(e.target.value), placeholder: "Catch the mega sale" }), _jsx(Input, { label: "Creative body", value: body, onChange: (e) => setBody(e.target.value), placeholder: "Up to 40% off this weekend" }), _jsx(Input, { label: "Image URL", value: imageUrl, onChange: (e) => setImageUrl(e.target.value), placeholder: "https://..." }), _jsx(Input, { label: "CTA label", value: ctaLabel, onChange: (e) => setCtaLabel(e.target.value), placeholder: "Shop now" }), _jsx(Input, { label: "CTA URL", value: ctaHref, onChange: (e) => setCtaHref(e.target.value), placeholder: "/promotions/deals" }), provider === "adsense" ? (_jsx(Input, { label: "AdSense slot", value: adsenseSlot, onChange: (e) => setAdsenseSlot(e.target.value), placeholder: "1234567890" })) : null, provider === "thirdParty" ? (_jsx(Input, { label: "Third-party URL", value: thirdPartyUrl, onChange: (e) => setThirdPartyUrl(e.target.value), placeholder: "https://adnetwork.example/slot" })) : null] }));
|
|
28
|
+
}
|
|
8
29
|
export function AdminAdEditorView({ adId, endpointBase = ADMIN_ENDPOINTS.ADS, labels = {}, onSaved, ...rest }) {
|
|
9
30
|
const [name, setName] = React.useState("");
|
|
10
31
|
const [provider, setProvider] = React.useState("manual");
|
|
@@ -157,20 +178,7 @@ export function AdminAdEditorView({ adId, endpointBase = ADMIN_ENDPOINTS.ADS, la
|
|
|
157
178
|
}
|
|
158
179
|
}
|
|
159
180
|
if (provider === "thirdParty") {
|
|
160
|
-
|
|
161
|
-
issues.push("Third-party ads require a third-party URL");
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
try {
|
|
165
|
-
const parsed = new URL(thirdPartyUrl.trim());
|
|
166
|
-
if (parsed.protocol !== "https:") {
|
|
167
|
-
issues.push("Third-party ad URL must be https");
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
issues.push("Third-party ad URL must be a valid URL");
|
|
172
|
-
}
|
|
173
|
-
}
|
|
181
|
+
validateThirdPartyUrl(thirdPartyUrl, issues);
|
|
174
182
|
if (!providerCredentialStatus?.hasThirdPartyScriptUrl) {
|
|
175
183
|
issues.push("Third-party provider script URL is missing in ad settings");
|
|
176
184
|
}
|
|
@@ -200,6 +208,12 @@ export function AdminAdEditorView({ adId, endpointBase = ADMIN_ENDPOINTS.ADS, la
|
|
|
200
208
|
? prev.filter((id) => id !== placementId)
|
|
201
209
|
: [...prev, placementId]);
|
|
202
210
|
};
|
|
211
|
+
const scheduleSection = renderAdScheduleSection({ startAt, setStartAt, endAt, setEndAt });
|
|
212
|
+
const creativeSection = renderAdCreativeSection({
|
|
213
|
+
title, setTitle, body, setBody, imageUrl, setImageUrl,
|
|
214
|
+
ctaLabel, setCtaLabel, ctaHref, setCtaHref,
|
|
215
|
+
adsenseSlot, setAdsenseSlot, thirdPartyUrl, setThirdPartyUrl, provider,
|
|
216
|
+
});
|
|
203
217
|
return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: labels.title ?? (adId ? "Edit Ad" : "Create Ad"), sections: [
|
|
204
218
|
metaQuery.error ? (_jsx(Alert, { variant: "error", title: "Could not load ad metadata", children: metaQuery.error instanceof Error ? metaQuery.error.message : "Unknown error" })) : null,
|
|
205
219
|
adQuery.error ? (_jsx(Alert, { variant: "error", title: "Could not load ad", children: adQuery.error instanceof Error ? adQuery.error.message : "Unknown error" })) : null,
|
|
@@ -219,6 +233,6 @@ export function AdminAdEditorView({ adId, endpointBase = ADMIN_ENDPOINTS.ADS, la
|
|
|
219
233
|
{ label: "Active", value: "active" },
|
|
220
234
|
{ label: "Scheduled", value: "scheduled" },
|
|
221
235
|
{ label: "Paused", value: "paused" },
|
|
222
|
-
], onChange: (event) => setStatus(event.target.value) }), _jsx(Input, { label: "Priority", type: "number", value: String(priority), onChange: (event) => setPriority(Number(event.target.value || 0)) })] }), _jsx(Toggle, { checked: requiresConsent, onChange: setRequiresConsent, label: "Require user ad-consent for this ad" }), _jsxs("div", { className: "space-y-2", "data-section": "adminadeditorview-div-236", children: [_jsx(Text, { className: "text-sm font-medium", children: "Placements" }), _jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-2", "data-section": "adminadeditorview-div-237", children: placements.map((placement) => (_jsxs("label", { className: "flex items-center gap-2 rounded-md border border-neutral-200 dark:border-slate-700 px-3 py-2", children: [_jsx("input", { type: "checkbox", checked: selectedPlacements.includes(placement.id), onChange: () => togglePlacement(placement.id) }), _jsx("span", { className: "text-sm", children: placement.label })] }, placement.id))) })] }),
|
|
236
|
+
], onChange: (event) => setStatus(event.target.value) }), _jsx(Input, { label: "Priority", type: "number", value: String(priority), onChange: (event) => setPriority(Number(event.target.value || 0)) })] }), _jsx(Toggle, { checked: requiresConsent, onChange: setRequiresConsent, label: "Require user ad-consent for this ad" }), _jsxs("div", { className: "space-y-2", "data-section": "adminadeditorview-div-236", children: [_jsx(Text, { className: "text-sm font-medium", children: "Placements" }), _jsx("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-2", "data-section": "adminadeditorview-div-237", children: placements.map((placement) => (_jsxs("label", { className: "flex items-center gap-2 rounded-md border border-neutral-200 dark:border-slate-700 px-3 py-2", children: [_jsx("input", { type: "checkbox", checked: selectedPlacements.includes(placement.id), onChange: () => togglePlacement(placement.id) }), _jsx("span", { className: "text-sm", children: placement.label })] }, placement.id))) })] }), scheduleSection, creativeSection, _jsxs("div", { className: "rounded-lg border border-neutral-200 dark:border-slate-700 p-3", "data-section": "adminadeditorview-div-240", children: [_jsx(Text, { className: "text-sm font-medium mb-1", children: "Preview" }), _jsx(Text, { className: "text-xs text-neutral-500 dark:text-zinc-400", children: title || name || "Untitled ad" }), body ? _jsx(Text, { className: "text-sm mt-1", children: body }) : null, ctaLabel ? _jsxs(Text, { className: "text-xs mt-1", children: ["CTA: ", ctaLabel, " (", ctaHref || "#", ")"] }) : null] }), publishHardeningRequired && publishIssues.length > 0 ? (_jsx(Alert, { variant: "warning", title: "Publish readiness issues", children: publishIssues.join("; ") })) : null, _jsx(FormActions, { align: "right", children: _jsx(Button, { type: "submit", disabled: saveMutation.isPending || blockedByPublishValidation || !name.trim() || selectedPlacements.length === 0, children: saveMutation.isPending ? "Saving..." : status === "active" ? "Publish ad" : adId ? "Save changes" : "Create ad" }) })] }),
|
|
223
237
|
] }));
|
|
224
238
|
}
|
|
@@ -6,6 +6,26 @@ import { Alert, Button, Input, Select, StackedViewShell, Text } from "../../../u
|
|
|
6
6
|
import { apiClient } from "../../../http";
|
|
7
7
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
8
|
import { DataTable } from "./DataTable";
|
|
9
|
+
function AdsSettingsPanel({ adsenseClientId, setAdsenseClientId, thirdPartyScriptUrl, setThirdPartyScriptUrl, consentRequired, setConsentRequired, serverCredentialIssues, localCredentialIssues, credentialStatus, providerCredentialsMasked, settingsMutation, hasPendingCredentialInput, currentConsentRequired, settingsMessage, onSave, }) {
|
|
10
|
+
return (_jsxs("div", { className: "rounded-lg border border-neutral-200 dark:border-slate-700 p-3 space-y-3", "data-section": "adminadsview-div-245", children: [_jsx(Text, { className: "text-sm font-semibold", children: "Provider and publish settings" }), _jsx(Text, { className: "text-xs text-neutral-500 dark:text-zinc-400", children: "Save provider credentials here before publishing AdSense or third-party inventory." }), serverCredentialIssues.length > 0 ? (_jsx(Alert, { variant: "warning", title: "Provider credentials need attention", children: serverCredentialIssues.join("; ") })) : null, _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3", "data-section": "adminadsview-div-246", children: [_jsx(Input, { label: "AdSense client id", value: adsenseClientId, onChange: (event) => setAdsenseClientId(event.target.value), placeholder: providerCredentialsMasked?.adsenseClientId || "ca-pub-XXXXXXXXXX" }), _jsx(Input, { label: "Third-party script URL", value: thirdPartyScriptUrl, onChange: (event) => setThirdPartyScriptUrl(event.target.value), placeholder: providerCredentialsMasked?.thirdPartyScriptUrl || "https://..." })] }), _jsxs(Text, { className: "text-xs text-neutral-500 dark:text-zinc-400", children: ["Stored credentials: AdSense ", credentialStatus?.hasAdsenseClientId ? "configured" : "missing", " \u00B7 Third-party ", credentialStatus?.hasThirdPartyScriptUrl ? "configured" : "missing"] }), _jsxs("div", { className: "flex items-center justify-between gap-3", "data-section": "adminadsview-div-247", children: [_jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: consentRequired, onChange: (event) => setConsentRequired(event.target.checked) }), "Require consent globally for ad rendering"] }), _jsx(Button, { size: "sm", variant: "outline", disabled: settingsMutation.isPending || localCredentialIssues.length > 0 || (!hasPendingCredentialInput && consentRequired === Boolean(currentConsentRequired)), onClick: onSave, children: settingsMutation.isPending ? "Saving..." : "Save settings" })] }), localCredentialIssues.length > 0 ? (_jsx(Alert, { variant: "error", title: "Fix settings before saving", children: localCredentialIssues.join("; ") })) : null, settingsMessage ? (_jsx(Alert, { variant: settingsMessage.toLowerCase().includes("failed") ? "error" : "success", title: "Settings", children: settingsMessage })) : null] }));
|
|
11
|
+
}
|
|
12
|
+
function AdsFilterRow({ q, setQ, status, setStatus, provider, setProvider, placement, setPlacement, placements, onPageReset }) {
|
|
13
|
+
return (_jsxs("div", { className: "grid grid-cols-1 md:grid-cols-4 gap-3", "data-section": "adminadsview-div-248", children: [_jsx(Input, { label: "Search", value: q, onChange: (event) => { onPageReset(); setQ(event.target.value); }, placeholder: "Search ads" }), _jsx(Select, { label: "Status", value: status, options: [
|
|
14
|
+
{ label: "All", value: "all" },
|
|
15
|
+
{ label: "Draft", value: "draft" },
|
|
16
|
+
{ label: "Active", value: "active" },
|
|
17
|
+
{ label: "Scheduled", value: "scheduled" },
|
|
18
|
+
{ label: "Paused", value: "paused" },
|
|
19
|
+
], onChange: (event) => { onPageReset(); setStatus(event.target.value); } }), _jsx(Select, { label: "Provider", value: provider, options: [
|
|
20
|
+
{ label: "All", value: "all" },
|
|
21
|
+
{ label: "Manual", value: "manual" },
|
|
22
|
+
{ label: "AdSense", value: "adsense" },
|
|
23
|
+
{ label: "Third Party", value: "thirdParty" },
|
|
24
|
+
], onChange: (event) => { onPageReset(); setProvider(event.target.value); } }), _jsx(Select, { label: "Placement", value: placement, options: [
|
|
25
|
+
{ label: "All", value: "all" },
|
|
26
|
+
...placements.map((item) => ({ label: item.label, value: item.id })),
|
|
27
|
+
], onChange: (event) => { onPageReset(); setPlacement(event.target.value); } })] }));
|
|
28
|
+
}
|
|
9
29
|
export function AdminAdsView({ endpoint = ADMIN_ENDPOINTS.ADS, labels = {}, createHref = "/admin/ads/new", renderEditLink, ...rest }) {
|
|
10
30
|
const queryClient = useQueryClient();
|
|
11
31
|
const [page, setPage] = React.useState(1);
|
|
@@ -132,34 +152,8 @@ export function AdminAdsView({ endpoint = ADMIN_ENDPOINTS.ADS, labels = {}, crea
|
|
|
132
152
|
return (_jsx(StackedViewShell, { portal: "admin", ...rest, title: labels.title ?? "Ad Inventory", sections: [
|
|
133
153
|
_jsxs("div", { className: "flex items-center justify-between gap-3", "data-section": "adminadsview-div-244", children: [_jsx(Text, { variant: "secondary", children: "Manage ad inventory, placement mapping, and publishing state." }), _jsx("a", { href: createHref, className: "inline-flex h-9 items-center rounded-md bg-neutral-900 px-3 text-sm font-medium text-white dark:bg-zinc-100 dark:text-zinc-900", children: "New ad" })] }),
|
|
134
154
|
adsQuery.error ? (_jsx(Alert, { variant: "error", title: "Could not load ads", children: adsQuery.error instanceof Error ? adsQuery.error.message : "Unknown error" })) : null,
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
setPage(1);
|
|
138
|
-
setQ(event.target.value);
|
|
139
|
-
}, placeholder: "Search ads" }), _jsx(Select, { label: "Status", value: status, options: [
|
|
140
|
-
{ label: "All", value: "all" },
|
|
141
|
-
{ label: "Draft", value: "draft" },
|
|
142
|
-
{ label: "Active", value: "active" },
|
|
143
|
-
{ label: "Scheduled", value: "scheduled" },
|
|
144
|
-
{ label: "Paused", value: "paused" },
|
|
145
|
-
], onChange: (event) => {
|
|
146
|
-
setPage(1);
|
|
147
|
-
setStatus(event.target.value);
|
|
148
|
-
} }), _jsx(Select, { label: "Provider", value: provider, options: [
|
|
149
|
-
{ label: "All", value: "all" },
|
|
150
|
-
{ label: "Manual", value: "manual" },
|
|
151
|
-
{ label: "AdSense", value: "adsense" },
|
|
152
|
-
{ label: "Third Party", value: "thirdParty" },
|
|
153
|
-
], onChange: (event) => {
|
|
154
|
-
setPage(1);
|
|
155
|
-
setProvider(event.target.value);
|
|
156
|
-
} }), _jsx(Select, { label: "Placement", value: placement, options: [
|
|
157
|
-
{ label: "All", value: "all" },
|
|
158
|
-
...placements.map((item) => ({ label: item.label, value: item.id })),
|
|
159
|
-
], onChange: (event) => {
|
|
160
|
-
setPage(1);
|
|
161
|
-
setPlacement(event.target.value);
|
|
162
|
-
} })] }),
|
|
155
|
+
_jsx(AdsSettingsPanel, { adsenseClientId: adsenseClientId, setAdsenseClientId: setAdsenseClientId, thirdPartyScriptUrl: thirdPartyScriptUrl, setThirdPartyScriptUrl: setThirdPartyScriptUrl, consentRequired: consentRequired, setConsentRequired: setConsentRequired, serverCredentialIssues: serverCredentialIssues, localCredentialIssues: localCredentialIssues, credentialStatus: credentialStatus, providerCredentialsMasked: adsQuery.data?.providerCredentialsMasked, settingsMutation: settingsMutation, hasPendingCredentialInput: hasPendingCredentialInput, currentConsentRequired: Boolean(adsQuery.data?.consentRequired), settingsMessage: settingsMessage, onSave: saveSettings }),
|
|
156
|
+
_jsx(AdsFilterRow, { q: q, setQ: setQ, status: status, setStatus: setStatus, provider: provider, setProvider: setProvider, placement: placement, setPlacement: setPlacement, placements: placements, onPageReset: () => setPage(1) }),
|
|
163
157
|
_jsx(DataTable, { columns: columns, rows: rows, isLoading: adsQuery.isLoading, currentPage: adsQuery.data?.page ?? page, totalPages: adsQuery.data?.totalPages ?? 1, onPageChange: setPage, emptyLabel: "No ads found" }),
|
|
164
158
|
] }));
|
|
165
159
|
}
|
|
@@ -19,6 +19,11 @@ const SORT_OPTIONS = [
|
|
|
19
19
|
{ value: "submittedAt", label: "Oldest" },
|
|
20
20
|
];
|
|
21
21
|
const STATUS_OPTIONS = ADMIN_EVENT_ENTRY_STATUS_TABS;
|
|
22
|
+
function EntriesFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, pendingFilters, setPendingFilters, applyFilters, }) {
|
|
23
|
+
if (!filterOpen)
|
|
24
|
+
return null;
|
|
25
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: _jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }));
|
|
26
|
+
}
|
|
22
27
|
export function AdminAllEventEntriesView({ children, ...props }) {
|
|
23
28
|
const hasChildren = React.Children.count(children) > 0;
|
|
24
29
|
const [view, setView] = useState("table");
|
|
@@ -102,5 +107,5 @@ export function AdminAllEventEntriesView({ children, ...props }) {
|
|
|
102
107
|
{ label: "Waitlist", onClick: () => updateMutation.mutate({ id: er.id, status: "WAITLISTED" }) },
|
|
103
108
|
{ label: "Cancel", destructive: true, onClick: () => updateMutation.mutate({ id: er.id, status: "CANCELLED" }) },
|
|
104
109
|
] }));
|
|
105
|
-
} })] }),
|
|
110
|
+
} })] }), _jsx(EntriesFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters })] }));
|
|
106
111
|
}
|
|
@@ -5,7 +5,7 @@ import { X } from "lucide-react";
|
|
|
5
5
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
6
6
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
7
7
|
import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
|
|
8
|
-
import { FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, ConfirmDeleteModal, RowActionMenu, useToast } from "../../../ui";
|
|
8
|
+
import { BulkActionBar, FilterChipGroup, ListingToolbar, Pagination, ListingViewShell, ConfirmDeleteModal, RowActionMenu, useToast } from "../../../ui";
|
|
9
9
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
10
10
|
import { ADMIN_BID_STATUS_TABS } from "../constants/filter-tabs";
|
|
11
11
|
import { toRecordArray, toRelativeDate, toRupees, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
|
|
@@ -20,6 +20,11 @@ const SORT_OPTIONS = [
|
|
|
20
20
|
{ value: "-amount", label: "Highest amount" },
|
|
21
21
|
];
|
|
22
22
|
const STATUS_OPTIONS = ADMIN_BID_STATUS_TABS;
|
|
23
|
+
function BidsFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, pendingFilters, setPendingFilters, applyFilters, }) {
|
|
24
|
+
if (!filterOpen)
|
|
25
|
+
return null;
|
|
26
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: _jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }));
|
|
27
|
+
}
|
|
23
28
|
export function AdminBidsView({ children, ...props }) {
|
|
24
29
|
const hasChildren = React.Children.count(children) > 0;
|
|
25
30
|
const [view, setView] = useState("table");
|
|
@@ -100,7 +105,9 @@ export function AdminBidsView({ children, ...props }) {
|
|
|
100
105
|
if (hasChildren) {
|
|
101
106
|
return _jsx(ListingViewShell, { portal: "admin", ...props, children: children });
|
|
102
107
|
}
|
|
103
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search bids, products, or bidder IDs", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }),
|
|
108
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search bids, products, or bidder IDs", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
109
|
+
{ id: "cancel", label: "Cancel Selected", variant: "secondary", onClick: () => { selection.clearSelection(); } },
|
|
110
|
+
] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), _jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No bids found", renderRowActions: (row) => {
|
|
104
111
|
const bidRow = row;
|
|
105
112
|
const isCancelled = bidRow.status === "cancelled" || bidRow.status === "voided";
|
|
106
113
|
return (_jsx(RowActionMenu, { actions: [
|
|
@@ -114,6 +121,6 @@ export function AdminBidsView({ children, ...props }) {
|
|
|
114
121
|
},
|
|
115
122
|
},
|
|
116
123
|
] }));
|
|
117
|
-
} })] }),
|
|
124
|
+
} })] }), _jsx(BidsFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters })] }), _jsx(ConfirmDeleteModal, { isOpen: cancelOpen, onClose: () => { setCancelOpen(false); setSelectedRow(null); }, onConfirm: () => { if (selectedRow)
|
|
118
125
|
cancelMutation.mutate(selectedRow.id); }, isDeleting: cancelMutation.isPending, title: "Cancel this bid?", message: "This will mark the bid as cancelled and notify the bidder. The auction will continue with remaining active bids.", confirmText: "Cancel bid", variant: "warning" })] }));
|
|
119
126
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
5
|
-
import { Button, Form, Input, RichTextEditor, Select, StackedViewShell, TagInput, Toggle, useToast
|
|
5
|
+
import { Button, Form, Input, RichTextEditor, Select, StackedViewShell, TagInput, Text, Toggle, useToast } from "../../../ui";
|
|
6
6
|
import { ImageUpload } from "../../media/upload/ImageUpload";
|
|
7
7
|
import { useMediaUpload } from "../../media";
|
|
8
8
|
import { apiClient } from "../../../http";
|
|
@@ -152,7 +152,7 @@ export function AdminBlogEditorView({ postId, onSaved, onDeleted, embedded, ...r
|
|
|
152
152
|
}, className: "space-y-5", children: [_jsx(Input, { label: "Title", value: title, onChange: (e) => handleTitleChange(e.target.value), required: true, placeholder: "e.g. How to Grade Pok\u00E9mon Cards" }), _jsx(Input, { label: "Slug", value: slug, onChange: (e) => {
|
|
153
153
|
setSlug(e.target.value);
|
|
154
154
|
setSlugManual(true);
|
|
155
|
-
}, placeholder: "blog-how-to-grade-pokemon-cards", helperText: "Auto-generated from title. Must start with 'blog-'." }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(Select, { label: "Category", options: CATEGORY_OPTIONS, value: category, onValueChange: (v) => setCategory(v) }), _jsx(Select, { label: "Status", options: STATUS_OPTIONS, value: status, onValueChange: (v) => setStatus(v) })] }), _jsx(Input, { label: "Excerpt", value: excerpt, onChange: (e) => setExcerpt(e.target.value), placeholder: "Short summary shown in listings and cards" }), _jsx(ImageUpload, { label: "Cover image", currentImage: coverImage, onUpload: (file) => upload(file, "blog", true, { type: "blog-cover", title: title || slug, category }), onChange: setCoverImage }), _jsxs("div", { className: "space-y-1", children: [_jsx(
|
|
155
|
+
}, placeholder: "blog-how-to-grade-pokemon-cards", helperText: "Auto-generated from title. Must start with 'blog-'." }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(Select, { label: "Category", options: CATEGORY_OPTIONS, value: category, onValueChange: (v) => setCategory(v) }), _jsx(Select, { label: "Status", options: STATUS_OPTIONS, value: status, onValueChange: (v) => setStatus(v) })] }), _jsx(Input, { label: "Excerpt", value: excerpt, onChange: (e) => setExcerpt(e.target.value), placeholder: "Short summary shown in listings and cards" }), _jsx(ImageUpload, { label: "Cover image", currentImage: coverImage, onUpload: (file) => upload(file, "blog", true, { type: "blog-cover", title: title || slug, category }), onChange: setCoverImage }), _jsxs("div", { className: "space-y-1", children: [_jsx(Text, { className: "text-sm font-medium text-zinc-700 dark:text-zinc-300", children: "Content" }), _jsx(RichTextEditor, { value: content, onChange: setContent, placeholder: "Write your article here...", minHeightClassName: "min-h-[320px]" })] }), _jsx(TagInput, { label: "Tags", value: tags, onChange: setTags, placeholder: "e.g. pokemon, grading, tcg" }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(Input, { label: "Author name", value: authorName, onChange: (e) => setAuthorName(e.target.value), placeholder: "Author display name" }), _jsx(Input, { label: "Publish date (optional)", value: publishedAt, onChange: (e) => setPublishedAt(e.target.value), type: "date", helperText: "Auto-set to now when publishing." })] }), _jsx(Toggle, { label: "Featured post", checked: isFeatured, onChange: setIsFeatured }), _jsx(Input, { label: "Meta title (optional)", value: metaTitle, onChange: (e) => setMetaTitle(e.target.value), placeholder: "Defaults to post title" }), _jsx(Input, { label: "Meta description (optional)", value: metaDescription, onChange: (e) => setMetaDescription(e.target.value), placeholder: "SEO description \u2014 max 160 chars", maxLength: 160 }), _jsxs("div", { className: "flex gap-3 pt-2", children: [_jsx(Button, { type: "submit", isLoading: isSubmitting, disabled: !canSave || isSubmitting, children: isEdit ? "Save changes" : "Create post" }), isEdit && (_jsx(Button, { type: "button", variant: "danger", isLoading: deleteMutation.isPending, onClick: () => {
|
|
156
156
|
if (confirm("Delete this post? This cannot be undone.")) {
|
|
157
157
|
deleteMutation.mutate();
|
|
158
158
|
}
|
|
@@ -5,7 +5,7 @@ import { Plus, X } from "lucide-react";
|
|
|
5
5
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
6
|
import { usePanelUrlSync } from "../../../react/hooks/use-panel-url-sync";
|
|
7
7
|
import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
|
|
8
|
-
import { BulkActionBar, Button, FilterChipGroup,
|
|
8
|
+
import { BulkActionBar, Button, FilterChipGroup, Heading, ListingToolbar, ListingViewShell, Pagination, SideDrawer } from "../../../ui";
|
|
9
9
|
import { AdminViewCards } from "./AdminViewCards";
|
|
10
10
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
11
11
|
import { ADMIN_BLOG_STATUS_TABS } from "../constants/filter-tabs";
|
|
@@ -26,6 +26,11 @@ const FEATURED_TABS = [
|
|
|
26
26
|
{ id: "", label: "All" },
|
|
27
27
|
{ id: "true", label: "Featured only" },
|
|
28
28
|
];
|
|
29
|
+
function BlogFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, pendingFilters, setPendingFilters, applyFilters, }) {
|
|
30
|
+
if (!filterOpen)
|
|
31
|
+
return null;
|
|
32
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsxs("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: [_jsx(FilterChipGroup, { label: "Status", tabs: STATUS_OPTIONS, value: pendingFilters.status ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, status: id })) }), _jsx(FilterChipGroup, { label: "Featured", tabs: FEATURED_TABS, value: pendingFilters.isFeatured ?? "", onChange: (id) => setPendingFilters((p) => ({ ...p, isFeatured: id })), allId: "" })] }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }));
|
|
33
|
+
}
|
|
29
34
|
export function AdminBlogView({ children, getRowHref, ...props }) {
|
|
30
35
|
const hasChildren = React.Children.count(children) > 0;
|
|
31
36
|
const [view, setView] = useState("table");
|
|
@@ -99,8 +104,8 @@ export function AdminBlogView({ children, getRowHref, ...props }) {
|
|
|
99
104
|
if (hasChildren) {
|
|
100
105
|
return _jsx(ListingViewShell, { portal: "admin", ...props, children: children });
|
|
101
106
|
}
|
|
102
|
-
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search articles, authors, or tags", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState, extra: _jsxs(Button, { size: "sm", onClick: openCreatePanel, className: "flex items-center gap-1.5", children: [_jsx(Plus, { className: "h-4 w-4" }), "New Post"] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
107
|
+
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(Heading, { level: 1, className: "sr-only", children: "Blog Posts" }), _jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search articles, authors, or tags", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState, extra: _jsxs(Button, { size: "sm", onClick: openCreatePanel, className: "flex items-center gap-1.5", children: [_jsx(Plus, { className: "h-4 w-4" }), "New Post"] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
103
108
|
{ id: "publish", label: "Publish Selected", variant: "primary", onClick: () => { selection.clearSelection(); } },
|
|
104
109
|
{ id: "draft", label: "Move to Draft", variant: "secondary", onClick: () => { selection.clearSelection(); } },
|
|
105
|
-
] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No blog posts found", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), onRowClick: (row) => openEditPanel(row.id) })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No blog posts found", onRowClick: (row) => openEditPanel(row.id), selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }),
|
|
110
|
+
] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No blog posts found", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), onRowClick: (row) => openEditPanel(row.id) })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No blog posts found", onRowClick: (row) => openEditPanel(row.id), selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), _jsx(BlogFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters }), _jsx(SideDrawer, { isOpen: isCreateOpen || isEditOpen, onClose: closePanel, title: isCreateOpen ? "New Post" : "Edit Post", mode: isCreateOpen ? "create" : "edit", children: (isCreateOpen || isEditOpen) && (_jsx(AdminBlogEditorView, { postId: editId ?? undefined, onSaved: closePanel, onDeleted: closePanel, embedded: true })) })] }));
|
|
106
111
|
}
|
|
@@ -5,7 +5,7 @@ import { Plus, X } from "lucide-react";
|
|
|
5
5
|
import { useUrlTable } from "../../../react/hooks/useUrlTable";
|
|
6
6
|
import { usePanelUrlSync } from "../../../react/hooks/use-panel-url-sync";
|
|
7
7
|
import { useBulkSelection } from "../../../react/hooks/useBulkSelection";
|
|
8
|
-
import { BulkActionBar, Button, ListingToolbar,
|
|
8
|
+
import { BulkActionBar, Button, ListingToolbar, ListingViewShell, Pagination, SideDrawer, Text } from "../../../ui";
|
|
9
9
|
import { AdminViewCards } from "./AdminViewCards";
|
|
10
10
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
11
11
|
import { toRecordArray, toRelativeDate, toStringValue, useAdminListingData, } from "../hooks/useAdminListingData";
|
|
@@ -20,6 +20,11 @@ const SORT_OPTIONS = [
|
|
|
20
20
|
{ value: "-name", label: "Name Z–A" },
|
|
21
21
|
{ value: "-createdAt", label: "Newest" },
|
|
22
22
|
];
|
|
23
|
+
function BrandsFilterDrawer({ filterOpen, setFilterOpen, activeFilterCount, clearFilters, pendingFilters, setPendingFilters, applyFilters, }) {
|
|
24
|
+
if (!filterOpen)
|
|
25
|
+
return null;
|
|
26
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "fixed inset-0 z-40 bg-black/40", "aria-hidden": "true", onClick: () => setFilterOpen(false) }), _jsxs("div", { className: "fixed inset-y-0 left-0 z-50 flex w-80 flex-col bg-white dark:bg-slate-900 shadow-2xl", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: [_jsx("span", { className: "text-base font-semibold text-zinc-900 dark:text-zinc-100", children: "Filters" }), _jsxs("div", { className: "flex items-center gap-2", children: [activeFilterCount > 0 && (_jsx("button", { type: "button", onClick: clearFilters, className: "text-xs text-zinc-500 hover:text-rose-500 dark:text-zinc-400 transition-colors", children: "Clear all" })), _jsx("button", { type: "button", onClick: () => setFilterOpen(false), "aria-label": "Close", className: "rounded-lg p-1.5 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-slate-800 transition-colors", children: _jsx(X, { className: "h-5 w-5" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto px-4 py-4 space-y-5", children: _jsxs("div", { className: "space-y-2", children: [_jsx(Text, { className: "text-xs font-semibold uppercase tracking-widest text-zinc-500 dark:text-zinc-400", children: "Status" }), _jsx("div", { className: "flex flex-wrap gap-2", children: [{ label: "All", value: "" }, { label: "Active", value: "true" }, { label: "Inactive", value: "false" }].map((opt) => (_jsx("button", { type: "button", onClick: () => setPendingFilters((p) => ({ ...p, isActive: opt.value })), className: `rounded-full px-3 py-1 text-xs font-medium border transition-colors ${(pendingFilters.isActive || "") === opt.value ? "bg-primary text-white border-primary" : "border-zinc-300 dark:border-slate-600 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-slate-800"}`, children: opt.label }, opt.label))) })] }) }), _jsx("div", { className: "border-t border-zinc-200 dark:border-slate-700 px-4 py-3.5", children: _jsxs("button", { type: "button", onClick: applyFilters, className: "w-full rounded-lg bg-primary py-2.5 text-sm font-semibold text-white hover:bg-primary-600 transition-colors active:scale-[0.98]", children: ["Apply Filters", activeFilterCount > 0 ? ` (${activeFilterCount})` : ""] }) })] })] }));
|
|
27
|
+
}
|
|
23
28
|
export function AdminBrandsView({ children, ...props }) {
|
|
24
29
|
const hasChildren = React.Children.count(children) > 0;
|
|
25
30
|
const [view, setView] = useState("table");
|
|
@@ -82,5 +87,5 @@ export function AdminBrandsView({ children, ...props }) {
|
|
|
82
87
|
return (_jsxs("div", { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: activeFilterCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search brands by name or slug", onSearchChange: setSearchInput, onSearchCommit: commitSearch, sortValue: table.get("sort") || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set("sort", v); }, showTableView: true, view: view, onViewChange: (v) => setView(v), onResetAll: resetAll, hasActiveState: hasActiveState, extra: _jsxs(Button, { size: "sm", onClick: openCreatePanel, className: "flex items-center gap-1.5", children: [_jsx(Plus, { className: "h-4 w-4" }), "Add Brand"] }) }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [
|
|
83
88
|
{ id: "edit", label: "Edit Brand", variant: "primary", onClick: () => { const id = selection.selectedIds[0]; if (id)
|
|
84
89
|
openEditPanel(id); selection.clearSelection(); } },
|
|
85
|
-
] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No brands found", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), onRowClick: (row) => openEditPanel(row.id) })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No brands found", onRowClick: (row) => openEditPanel(row.id), selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }),
|
|
90
|
+
] }), totalPages > 1 && (_jsx("div", { className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 flex justify-center bg-white/95 dark:bg-slate-900/95 backdrop-blur-sm border-b border-zinc-200 dark:border-slate-700 px-3 py-1.5", children: _jsx(Pagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsxs("div", { className: "py-4 px-3 sm:px-4", children: [errorMessage && (_jsx("div", { className: "mb-4 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-950/40 dark:text-red-200", children: errorMessage })), view === "table" ? (_jsx(DataTable, { rows: rows, isLoading: isLoading, emptyLabel: "No brands found", selectedIds: selection.selectedIdSet, onToggleSelect: selection.toggle, onToggleSelectAll: (next) => next ? selection.setSelectedIds(rows.map(r => r.id)) : selection.clearSelection(), onRowClick: (row) => openEditPanel(row.id) })) : (_jsx(AdminViewCards, { rows: rows, view: view, isLoading: isLoading, emptyLabel: "No brands found", onRowClick: (row) => openEditPanel(row.id), selectedIdSet: selection.selectedIdSet, onToggleSelect: selection.toggle }))] }), _jsx(BrandsFilterDrawer, { filterOpen: filterOpen, setFilterOpen: setFilterOpen, activeFilterCount: activeFilterCount, clearFilters: clearFilters, pendingFilters: pendingFilters, setPendingFilters: setPendingFilters, applyFilters: applyFilters }), _jsx(SideDrawer, { isOpen: isCreateOpen || isEditOpen, onClose: closePanel, title: isCreateOpen ? "Add Brand" : "Edit Brand", mode: isCreateOpen ? "create" : "edit", children: (isCreateOpen || isEditOpen) && (_jsx(AdminBrandEditorView, { brandId: editId ?? undefined, onSaved: closePanel, onDeleted: closePanel, embedded: true })) })] }));
|
|
86
91
|
}
|
|
@@ -37,6 +37,10 @@ const EMPTY_FORM = {
|
|
|
37
37
|
isActive: true,
|
|
38
38
|
coverImage: "",
|
|
39
39
|
};
|
|
40
|
+
async function parseResponseError(res) {
|
|
41
|
+
const err = (await res.json().catch(() => null));
|
|
42
|
+
return err?.error?.message ?? `Create failed: ${res.status}`;
|
|
43
|
+
}
|
|
40
44
|
function bundleToForm(bundle) {
|
|
41
45
|
if (!bundle)
|
|
42
46
|
return EMPTY_FORM;
|
|
@@ -158,8 +162,7 @@ export function AdminBundleEditorView({ bundleId, onSaved, onDeleted, }) {
|
|
|
158
162
|
body: JSON.stringify(body),
|
|
159
163
|
});
|
|
160
164
|
if (!res.ok) {
|
|
161
|
-
|
|
162
|
-
throw new Error(err?.error?.message ?? `Create failed: ${res.status}`);
|
|
165
|
+
throw new Error(await parseResponseError(res));
|
|
163
166
|
}
|
|
164
167
|
const json = (await res.json());
|
|
165
168
|
const newId = json?.data?.id;
|