@mohasinac/appkit 3.2.7 → 3.2.9
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/filters/filter-load-options.js +11 -9
- package/dist/_internal/client/theme/ThemeProvider.js +14 -4
- package/dist/_internal/server/features/auth/firebase-identity.d.ts +12 -0
- package/dist/_internal/server/features/auth/firebase-identity.js +25 -0
- package/dist/_internal/server/features/site-settings/safe-settings.d.ts +12 -0
- package/dist/_internal/server/features/site-settings/safe-settings.js +25 -0
- package/dist/_internal/shared/features/layout/config.js +7 -7
- package/dist/_internal/shared/styles/skeleton.d.ts +8 -8
- package/dist/_internal/shared/styles/skeleton.js +10 -10
- package/dist/_internal/shared/styles/themed.d.ts +12 -12
- package/dist/_internal/shared/styles/themed.js +12 -12
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/about/components/FAQPageView.js +1 -1
- package/dist/features/about/components/FeesView.js +2 -2
- package/dist/features/about/components/HowAuctionsWorkView.js +2 -2
- package/dist/features/about/components/HowCheckoutWorksView.js +2 -2
- package/dist/features/about/components/HowOffersWorkView.js +2 -2
- package/dist/features/about/components/HowOrdersWorkView.js +2 -2
- package/dist/features/about/components/HowPayoutsWorkView.js +2 -2
- package/dist/features/about/components/HowPreOrdersWorkView.js +2 -2
- package/dist/features/about/components/HowReviewsWorkView.js +2 -2
- package/dist/features/about/components/PublicProfileView.js +3 -3
- package/dist/features/about/components/SecurityPrivacyView.js +2 -2
- package/dist/features/account/components/AddressBook.js +2 -2
- package/dist/features/account/components/AddressesIndexListing.js +1 -1
- package/dist/features/account/components/ChatWindow.js +1 -1
- package/dist/features/account/components/MessagesView.js +1 -1
- package/dist/features/account/components/NotificationBell.js +2 -2
- package/dist/features/account/components/NotificationPreferencesPanel.js +1 -1
- package/dist/features/account/components/UserSidebar.js +3 -3
- package/dist/features/account/components/UserSupportView.js +3 -3
- package/dist/features/admin/components/AdminAdEditorView.js +1 -1
- package/dist/features/admin/components/AdminAdsView.js +5 -5
- package/dist/features/admin/components/AdminAnalyticsView.js +1 -1
- package/dist/features/admin/components/AdminBrandsView.js +1 -1
- package/dist/features/admin/components/AdminBundlesView.js +2 -2
- package/dist/features/admin/components/AdminCarouselEditorView.js +4 -4
- package/dist/features/admin/components/AdminCarouselView.js +2 -2
- package/dist/features/admin/components/AdminCategoriesView.js +2 -2
- package/dist/features/admin/components/AdminContactEditorView.js +1 -1
- package/dist/features/admin/components/AdminCouponEditorView.js +1 -1
- package/dist/features/admin/components/AdminEmployeeEditorView.js +3 -3
- package/dist/features/admin/components/AdminFaqsView.js +1 -1
- package/dist/features/admin/components/AdminFeatureEditorView.js +2 -2
- package/dist/features/admin/components/AdminFeatureFlagsView.js +1 -1
- package/dist/features/admin/components/AdminFeaturesView.js +2 -2
- package/dist/features/admin/components/AdminNavigationView.js +2 -2
- package/dist/features/admin/components/AdminNotificationsView.js +1 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +1 -1
- package/dist/features/admin/components/AdminPageHeader.d.ts +1 -1
- package/dist/features/admin/components/AdminPageHeader.js +1 -1
- package/dist/features/admin/components/AdminReviewsView.js +1 -1
- package/dist/features/admin/components/AdminScammerEditorView.js +3 -3
- package/dist/features/admin/components/AdminScammersView.js +1 -1
- package/dist/features/admin/components/AdminSectionsView.js +1 -1
- package/dist/features/admin/components/AdminSessionsView.js +1 -1
- package/dist/features/admin/components/AdminSidebar.js +5 -5
- package/dist/features/admin/components/AdminSiteSettingsView.js +3 -3
- package/dist/features/admin/components/AdminStoreEditorView.js +3 -3
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +3 -3
- package/dist/features/admin/components/AdminSupportTicketsView.js +2 -2
- package/dist/features/admin/components/AdminViewCards.js +5 -5
- package/dist/features/admin/components/DataTable.js +1 -1
- package/dist/features/admin/components/DrawerFormFooter.js +1 -1
- package/dist/features/admin/components/QuickActionsPanel.js +1 -1
- package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
- package/dist/features/auctions/components/CollapsibleBidHistory.js +1 -1
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +1 -1
- package/dist/features/auth/components/ForgotPasswordView.js +1 -1
- package/dist/features/auth/components/Guards.js +1 -1
- package/dist/features/auth/components/RegisterForm.js +1 -1
- package/dist/features/auth/components/ResetPasswordView.js +1 -1
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/blog/components/BlogIndexListing.js +1 -1
- package/dist/features/blog/components/BlogListView.js +3 -3
- package/dist/features/blog/components/BlogPostView.js +1 -1
- package/dist/features/cart/components/CartDrawer.js +5 -5
- package/dist/features/categories/components/BrandDetailPageView.js +1 -1
- package/dist/features/categories/components/BrandDetailTabs.js +1 -1
- package/dist/features/categories/components/CategoriesIndexListing.js +2 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +1 -1
- package/dist/features/categories/components/CategoryDetailTabs.js +1 -1
- package/dist/features/categories/components/CategoryGrid.js +3 -3
- package/dist/features/categories/components/CategoryTree.js +2 -2
- package/dist/features/categories/components/ConcernCard.js +1 -1
- package/dist/features/checkout/components/CheckoutStepper.js +1 -1
- package/dist/features/classified/components/ClassifiedIndexListing.js +1 -1
- package/dist/features/collections/components/CollectionCard.js +1 -1
- package/dist/features/collections/hooks/useCollections.js +12 -12
- package/dist/features/contact/components/ContactForm.js +1 -1
- package/dist/features/copilot/components/AdminCopilotView.js +5 -5
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +1 -1
- package/dist/features/events/components/AdminEventEntriesView.js +1 -1
- package/dist/features/events/components/EventCard.js +1 -1
- package/dist/features/events/components/EventPollWidget.js +1 -1
- package/dist/features/events/components/EventStatusBadge.js +1 -1
- package/dist/features/events/components/EventsIndexListing.js +1 -1
- package/dist/features/faq/components/ContactCTA.js +2 -2
- package/dist/features/faq/components/FAQAccordion.js +4 -4
- package/dist/features/faq/components/FAQCategorySidebar.js +2 -2
- package/dist/features/faq/components/FAQHelpfulButtons.js +4 -4
- package/dist/features/faq/components/RelatedFAQs.js +1 -1
- package/dist/features/filters/AsyncFacetSection.js +3 -3
- package/dist/features/filters/FilterFacetSection.js +5 -5
- package/dist/features/filters/RangeFilter.js +3 -3
- package/dist/features/filters/SwitchFilter.js +2 -2
- package/dist/features/forms/Checkbox.js +1 -1
- package/dist/features/forms/Input.js +2 -2
- package/dist/features/forms/Radio.js +2 -2
- package/dist/features/forms/Select.js +1 -1
- package/dist/features/forms/Slider.js +1 -1
- package/dist/features/forms/Textarea.js +1 -1
- package/dist/features/forms/Toggle.js +3 -3
- package/dist/features/forms/utils.d.ts +4 -4
- package/dist/features/forms/utils.js +4 -4
- package/dist/features/homepage/components/AdSlot.js +1 -1
- package/dist/features/homepage/components/AdvertisementBanner.js +3 -3
- package/dist/features/homepage/components/BrandsSection.js +2 -2
- package/dist/features/homepage/components/GoogleReviewsSection.js +1 -1
- package/dist/features/homepage/components/HeroSection.js +1 -1
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
- package/dist/features/homepage/components/HowItWorksSection.js +1 -1
- package/dist/features/homepage/components/ShopByCategorySection.js +2 -2
- package/dist/features/homepage/components/SiteFeaturesSection.js +1 -1
- package/dist/features/homepage/components/SocialPostCard.js +1 -1
- package/dist/features/homepage/components/StatsCounterSection.js +1 -1
- package/dist/features/homepage/components/TrustIndicatorsSection.js +1 -1
- package/dist/features/homepage/constants/hero-carousel-styles.d.ts +3 -3
- package/dist/features/homepage/constants/hero-carousel-styles.js +3 -3
- package/dist/features/homepage/hooks/useActiveAd.d.ts +1 -3
- package/dist/features/homepage/hooks/useActiveAd.js +9 -17
- package/dist/features/layout/AppLayoutShell.js +12 -12
- package/dist/features/layout/AutoBreadcrumbs.js +1 -1
- package/dist/features/layout/BackToTop.js +2 -2
- package/dist/features/layout/BottomActions.js +8 -8
- package/dist/features/layout/BottomNavItem.js +1 -1
- package/dist/features/layout/BottomNavLayout.js +1 -1
- package/dist/features/layout/BottomNavbar.d.ts +1 -1
- package/dist/features/layout/BottomNavbar.js +2 -2
- package/dist/features/layout/BottomSheet.js +1 -1
- package/dist/features/layout/Breadcrumbs.js +2 -2
- package/dist/features/layout/FooterLayout.js +2 -2
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/NavItem.js +1 -1
- package/dist/features/layout/NavbarLayout.js +3 -3
- package/dist/features/layout/SidebarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +3 -3
- package/dist/features/live/components/LiveItemsIndexListing.js +1 -1
- package/dist/features/loyalty/components/CoinsDisplay.js +1 -1
- package/dist/features/loyalty/hooks/useLoyaltyBalance.js +11 -3
- package/dist/features/media/AvatarUpload.js +1 -1
- package/dist/features/media/MediaPickerModal.js +1 -1
- package/dist/features/media/components/MediaSlider.js +1 -1
- package/dist/features/media/finalize.d.ts +12 -1
- package/dist/features/media/finalize.js +64 -11
- package/dist/features/media/modals/ImageCropModal.js +1 -1
- package/dist/features/media/modals/ImageEditor.js +1 -1
- package/dist/features/media/repository/media-assets.repository.d.ts +33 -0
- package/dist/features/media/repository/media-assets.repository.js +43 -0
- package/dist/features/media/upload/MediaUploadField.js +1 -1
- package/dist/features/messages/hooks/useConversation.js +12 -17
- package/dist/features/messages/hooks/useConversations.js +3 -5
- package/dist/features/orders/components/MarketplaceOrderCard.js +1 -1
- package/dist/features/orders/components/OrdersList.js +3 -3
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -2
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +1 -1
- package/dist/features/pre-orders/hooks/usePreOrders.js +12 -12
- package/dist/features/products/columns/productTableColumns.js +3 -3
- package/dist/features/products/components/BidHistory.js +1 -1
- package/dist/features/products/components/CompareOverlay.js +9 -9
- package/dist/features/products/components/CustomSectionTabContent.js +1 -1
- package/dist/features/products/components/CustomSectionsEditor.js +1 -1
- package/dist/features/products/components/FeatureBadge.js +2 -2
- package/dist/features/products/components/GroupSettingsPanel.js +2 -2
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/PrizeDrawsIndexListing.js +1 -1
- package/dist/features/products/components/ProductDetailPageView.js +4 -4
- package/dist/features/products/components/ProductFeatureBadges.js +2 -2
- package/dist/features/products/components/ProductFeaturesSelector.js +4 -4
- package/dist/features/products/components/ProductGalleryClient.js +1 -1
- package/dist/features/products/components/ProductGrid.js +8 -8
- package/dist/features/products/components/ProductTabs.js +1 -1
- package/dist/features/products/components/ProductTabsShell.js +1 -1
- package/dist/features/products/components/ShareButton.js +1 -1
- package/dist/features/products/components/ShowGroupSection.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +2 -2
- package/dist/features/promotions/components/CouponCard.js +2 -2
- package/dist/features/promotions/components/CouponsIndexListing.js +2 -2
- package/dist/features/reviews/components/ReviewDetailPageView.js +1 -1
- package/dist/features/reviews/components/ReviewDetailShell.js +5 -5
- package/dist/features/reviews/components/ReviewModal.js +1 -1
- package/dist/features/reviews/components/ReviewSummary.js +2 -2
- package/dist/features/reviews/components/ReviewsIndexListing.js +1 -1
- package/dist/features/reviews/components/ReviewsList.js +2 -2
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/components/Search.js +11 -11
- package/dist/features/search/components/SearchFiltersRow.js +1 -1
- package/dist/features/search/components/SearchResultsSection.js +2 -2
- package/dist/features/seller/components/SellerAddressesView.js +4 -4
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +1 -1
- package/dist/features/seller/components/SellerCouponEditorView.js +1 -1
- package/dist/features/seller/components/SellerCouponsView.js +1 -1
- package/dist/features/seller/components/SellerFeaturesView.js +2 -2
- package/dist/features/seller/components/SellerGoogleReviewsView.js +1 -1
- package/dist/features/seller/components/SellerOrdersView.js +1 -1
- package/dist/features/seller/components/SellerPayoutMethodsView.js +1 -1
- package/dist/features/seller/components/SellerSidebar.js +4 -4
- package/dist/features/seller/components/analytics/SellerRevenueChart.js +1 -1
- package/dist/features/seller/components/analytics/SellerTopProducts.js +1 -1
- package/dist/features/site-settings/components/ActionPermissionsManager.js +2 -2
- package/dist/features/site-settings/components/NavPermissionsManager.js +1 -1
- package/dist/features/stores/components/InteractiveStoreCard.js +3 -3
- package/dist/features/stores/components/StoreHeader.js +2 -2
- package/dist/features/stores/components/StoreNavTabs.js +2 -2
- package/dist/features/stores/components/StorePreOrdersListing.js +1 -1
- package/dist/features/stores/components/StoreReviewsListing.js +1 -1
- package/dist/features/stores/components/StoresIndexListing.js +1 -1
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +2 -2
- package/dist/features/wishlist/components/WishlistPage.js +2 -2
- package/dist/features/wishlist/hooks/useWishlistCount.js +5 -13
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/http/retry.d.ts +1 -0
- package/dist/http/retry.js +26 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +8 -0
- package/dist/next/ErrorBoundary.js +1 -1
- package/dist/repositories/index.d.ts +2 -0
- package/dist/repositories/index.js +2 -0
- package/dist/seed/site-settings-seed-data.js +476 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +9 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/index.d.ts +6 -6
- package/dist/tokens/index.js +59 -59
- package/dist/ui/DataTable.js +3 -3
- package/dist/ui/components/Avatar.js +1 -1
- package/dist/ui/components/BaseListingCard.js +7 -7
- package/dist/ui/components/Button.js +3 -3
- package/dist/ui/components/DetailPageGallery.js +3 -3
- package/dist/ui/components/DetailPageHero.js +1 -1
- package/dist/ui/components/DetailPageTabs.js +4 -4
- package/dist/ui/components/FilterChipGroup.js +2 -2
- package/dist/ui/components/Input.js +1 -1
- package/dist/ui/components/Layout.d.ts +2 -2
- package/dist/ui/components/ListingFilterDrawer.js +2 -2
- package/dist/ui/components/ListingToolbar.js +6 -6
- package/dist/ui/components/LoginRequiredModal.js +1 -1
- package/dist/ui/components/Modal.js +1 -1
- package/dist/ui/components/NavbarChevron.js +1 -1
- package/dist/ui/components/PageLoader.js +3 -3
- package/dist/ui/components/Radio.js +1 -1
- package/dist/ui/components/RichTextEditor.js +1 -1
- package/dist/ui/components/Select.js +1 -1
- package/dist/ui/components/SettingsSection.js +3 -3
- package/dist/ui/components/SideModal.js +1 -1
- package/dist/ui/components/Slider.js +2 -2
- package/dist/ui/components/StatsGrid.js +1 -1
- package/dist/ui/components/Typography.js +3 -3
- package/dist/ui/components/surface-tokens.d.ts +11 -11
- package/dist/ui/components/surface-tokens.js +12 -12
- package/dist/utils/safe-fire-forget.d.ts +14 -0
- package/dist/utils/safe-fire-forget.js +23 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import Link from "next/link";
|
|
3
3
|
import { Avatar, BlockHeader, Button, Div, Row, Section, SiteLogo, Span } from "../../ui";
|
|
4
4
|
/** Shared icon-button class for all action buttons in the title bar. */
|
|
5
|
-
const iconBtn = "flex items-center justify-center w-9 h-9 rounded-lg text-
|
|
5
|
+
const iconBtn = "flex items-center justify-center w-9 h-9 rounded-lg text-[var(--appkit-color-text-muted)] hover:bg-primary-50 hover:text-primary-700 hover:bg-[var(--appkit-color-surface-elevated)] dark:hover:text-secondary-400 transition-colors";
|
|
6
6
|
/** Badge counter class for wishlist/cart counts. */
|
|
7
7
|
const countBadge = "absolute -top-0.5 -right-0.5 flex items-center justify-center w-4 h-4 rounded-full bg-error-surface text-white text-[10px] font-bold leading-none";
|
|
8
8
|
/**
|
|
@@ -30,10 +30,10 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
|
|
|
30
30
|
const wishlistEl = wishlistHref ? (_jsxs(Link, { href: wishlistHref, "aria-label": `Wishlist${wishlistCount > 0 ? `, ${wishlistCount} items` : ""}`, className: `relative ${iconBtn}`, children: [_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4.318 6.318a4.5 4.5 0 0 0 0 6.364L12 20.364l7.682-7.682a4.5 4.5 0 0 0-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 0 0-6.364 0z" }) }), wishlistCount > 0 && (_jsx(Span, { className: countBadge, children: wishlistCount > 9 ? "9+" : wishlistCount }))] })) : null;
|
|
31
31
|
const cartEl = cartHref ? (_jsxs(Link, { href: cartHref, "aria-label": `Cart${cartCount > 0 ? `, ${cartCount} items` : ""}`, className: `relative ${iconBtn}`, children: [_jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M2.25 3h1.386c.51 0 .955.343 1.087.835l.383 1.437M7.5 14.25a3 3 0 0 0-3 3h15.75m-12.75-3h11.218c1.121-2.3 2.1-4.684 2.924-7.138a60.114 60.114 0 0 0-16.536-1.84M7.5 14.25 5.106 5.272M6 20.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm12.75 0a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0z" }) }), cartCount > 0 && (_jsx(Span, { className: countBadge, children: cartCount > 9 ? "9+" : cartCount }))] })) : null;
|
|
32
32
|
const profileEl = profileHref ? (_jsx(Link, { href: profileHref, "aria-label": user ? `Profile — ${user.displayName ?? user.email}` : "Sign in", className: `relative ${iconBtn}`, children: _jsx(Avatar, { src: user?.photoURL ?? undefined, name: user?.displayName ?? user?.email ?? undefined, alt: user?.displayName ?? "Profile", size: "sm" }) })) : null;
|
|
33
|
-
const authButtonsEl = !user && (loginHref || registerHref) ? (_jsxs(Row, { gap: "xs", className: "hidden lg:flex", children: [loginHref && (_jsx(Link, { href: loginHref, className: "px-3 py-1.5 text-sm font-medium text-
|
|
33
|
+
const authButtonsEl = !user && (loginHref || registerHref) ? (_jsxs(Row, { gap: "xs", className: "hidden lg:flex", children: [loginHref && (_jsx(Link, { href: loginHref, className: "px-3 py-1.5 text-sm font-medium text-[var(--appkit-color-text-muted)] hover:text-primary-700 dark:hover:text-secondary-400 transition-colors rounded-lg hover:bg-primary-50 hover:bg-[var(--appkit-color-surface-elevated)]", children: "Sign in" })), registerHref && (_jsx(Link, { href: registerHref, className: "px-3 py-1.5 text-sm font-semibold rounded-lg bg-primary text-white hover:bg-primary/90 transition-colors btn-glow shadow-sm", children: "Register" }))] })) : null;
|
|
34
34
|
const hasTb2 = !!(notificationsEl || wishlistEl || cartEl || profileEl);
|
|
35
35
|
// ── Render ───────────────────────────────────────────────────────────────────
|
|
36
|
-
return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-white/95
|
|
36
|
+
return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-white/95 bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b border-zinc-100 border-[var(--appkit-color-border-subtle)] shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-xs text-center font-medium", padding: "y-2xs", children: promoStripText })), _jsxs(Div, { paddingX: "x-page", className: "container mx-auto max-w-[1920px]", children: [_jsxs(Row, { justify: "between", gap: "none", className: "relative h-14", children: [_jsx(Row, { gap: "3", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { title: brandName, size: "md" }) }) }), siteLogoUrl ? (_jsx(Row, { className: "absolute inset-y-0 left-1/2 -translate-x-1/2", align: "center", children: _jsx(Link, { href: logoHref, "aria-label": brandName, className: "flex items-center transition-opacity hover:opacity-80", children: _jsx(SiteLogo, { src: siteLogoUrl, title: brandName, size: "md" }) }) })) : (navSlot && _jsx(Div, { className: "hidden md:flex", children: navSlot })), _jsxs(Row, { gap: "xs", children: [devSlot, compareEl, notificationSlot, notificationsEl && _jsx(Div, { className: "hidden lg:flex", children: notificationsEl }), wishlistEl && _jsx(Div, { className: "hidden lg:flex", children: wishlistEl }), cartEl && _jsx(Div, { className: "hidden lg:flex", children: cartEl }), user
|
|
37
37
|
? profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })
|
|
38
38
|
: authButtonsEl ?? (profileEl && _jsx(Div, { className: "hidden lg:flex", children: profileEl })), searchBtn, promotionsEl, tourBtn, themeBtn, hamburgerBtn] })] }), hasTb2 && (_jsxs(Row, { border: "subtle", as: "nav", "aria-label": "Account actions", justify: "end", gap: "xs", className: "flex lg:hidden h-10 border-t px-1", children: [notificationsEl, wishlistEl, cartEl, profileEl] }))] })] }));
|
|
39
39
|
}
|
|
@@ -111,5 +111,5 @@ export function LiveItemsIndexListing({ initialData }) {
|
|
|
111
111
|
useBottomActions(selection.selectedCount > 0 ? { bulk: { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] } } : {});
|
|
112
112
|
return (_jsxs(Div, { className: "min-h-screen", children: [_jsx(ListingToolbar, { filterCount: filterActiveCount, onFiltersClick: openFilters, searchValue: searchInput, searchPlaceholder: "Search live items\u2026", onSearchChange: setSearchInput, onSearchCommit: commitSearch, onSearchKeyDown: (e) => { if (e.key === "Enter")
|
|
113
113
|
commitSearch(); }, sortValue: table.get(TABLE_KEYS.SORT) || DEFAULT_SORT, sortOptions: SORT_OPTIONS, onSortChange: (v) => { table.set(TABLE_KEYS.SORT, v); }, view: view, onViewChange: (v) => { if (v !== "table")
|
|
114
|
-
setView(v); }, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] }), totalPages > 1 && (_jsx(Row, { border: "bottom", className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 backdrop-blur-sm", surface: "default", padding: "toolbar", justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { className: gridClass, children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) })) : products.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "center", children: "No live item listings found." })) : (_jsx(Div, { className: view === "list" ? "flex flex-col divide-y divide-zinc-100
|
|
114
|
+
setView(v); }, onResetAll: resetAll, hasActiveState: hasActiveState, bulkMode: selection.isSelecting, bulkSelectedCount: selection.selectedCount, bulkTotalCount: products.length, onBulkSelectAll: selection.toggleAll, onBulkClear: selection.clearSelection }), _jsx(BulkActionBar, { selectedCount: selection.selectedCount, onClearSelection: selection.clearSelection, actions: [] }), totalPages > 1 && (_jsx(Row, { border: "bottom", className: "sticky top-[calc(var(--header-height,0px)+44px)] z-10 backdrop-blur-sm", surface: "default", padding: "toolbar", justify: "center", children: _jsx(Pagination, { currentPage: page, totalPages: totalPages, onPageChange: (p) => table.setPage(p) }) })), _jsx(Div, { padding: "y-lg", children: isLoading ? (_jsx(Div, { className: gridClass, children: Array.from({ length: 10 }).map((_, i) => (_jsxs(Div, { className: `${__O.hidden} animate-pulse`, border: "subtle", rounded: "xl", children: [_jsx(Div, { className: "aspect-square", surface: "subtle" }), _jsxs(Stack, { className: `${__P.p3}`, gap: "sm", children: [_jsx(Div, { className: "h-3 w-3/4", surface: "subtle", rounded: "default" }), _jsx(Div, { className: "h-4 w-1/3", surface: "subtle", rounded: "default" })] })] }, i))) })) : products.length === 0 ? (_jsx(Text, { paddingY: "3xl", color: "muted", size: "sm", align: "center", children: "No live item listings found." })) : (_jsx(Div, { className: view === "list" ? "flex flex-col divide-y divide-zinc-100 divide-[var(--appkit-color-border-subtle)] rounded-xl border border-[var(--appkit-color-border-subtle)]" : gridClass, children: products.map((product) => (_jsx(InteractiveProductCard, { product: product, href: String(ROUTES.PUBLIC.LIVE_DETAIL(product.id)), variant: view === "list" ? "list" : "grid", isWishlisted: wishlistActions.isWishlisted(product.id), onToggleWishlist: handleToggleWishlist, selectable: selection.isSelecting, isSelected: selection.isSelected(product.id), onSelect: (id) => selection.toggle(id) }, product.id))) })) }), _jsx(FilterDrawer, { open: filterOpen, onOpen: openFilters, onClose: () => setFilterOpen(false), onApply: applyFilters, onReset: onFilterClear, activeCount: filterActiveCount, hideTrigger: true, children: _jsx(LiveItemFilters, { table: pendingTable, categoryOptions: categoryOptions }) }), _jsx(LoginRequiredModal, { isOpen: modalOpen, onClose: closeModal, message: modalMessage })] }));
|
|
115
115
|
}
|
|
@@ -6,5 +6,5 @@ export function CoinsBadge({ coins, className }) {
|
|
|
6
6
|
return (_jsxs(Span, { className: `${CLS_COIN_PILL} ${className ?? ""}`, children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83E\uDE99" }), coins.toLocaleString()] }));
|
|
7
7
|
}
|
|
8
8
|
export function CoinsDisplay({ coins, label = "HC Coins" }) {
|
|
9
|
-
return (_jsxs(Stack, { align: "center", gap: "xs", children: [_jsx(Text, { className: CLS_COIN_TOTAL, children: coins.toLocaleString() }), _jsx(Text, { className: "text-
|
|
9
|
+
return (_jsxs(Stack, { align: "center", gap: "xs", children: [_jsx(Text, { className: CLS_COIN_TOTAL, children: coins.toLocaleString() }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: label })] }));
|
|
10
10
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import { useQuery } from "@tanstack/react-query";
|
|
3
|
+
import { apiClient } from "../../../http/ApiClient";
|
|
4
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
2
5
|
import { LOYALTY_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
3
6
|
export function useLoyaltyBalance(uid, opts) {
|
|
4
7
|
return useQuery({
|
|
@@ -7,10 +10,15 @@ export function useLoyaltyBalance(uid, opts) {
|
|
|
7
10
|
if (!uid)
|
|
8
11
|
return null;
|
|
9
12
|
const endpoint = opts?.endpoint ?? LOYALTY_ENDPOINTS.BALANCE(uid);
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
try {
|
|
14
|
+
return await apiClient.get(endpoint);
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
// loyalty balance is a display feature — treat any fetch failure as null
|
|
18
|
+
// so the loyalty section degrades gracefully without blocking the page
|
|
19
|
+
void normalizeError(err);
|
|
12
20
|
return null;
|
|
13
|
-
|
|
21
|
+
}
|
|
14
22
|
},
|
|
15
23
|
enabled: !!uid,
|
|
16
24
|
});
|
|
@@ -109,6 +109,6 @@ export function AvatarUpload({ currentPhotoURL, currentCropData, userId, display
|
|
|
109
109
|
fileInputRef.current.value = "";
|
|
110
110
|
};
|
|
111
111
|
const hasPending = pendingCropData !== null && pendingUploadFile !== null;
|
|
112
|
-
return (_jsxs(_Fragment, { children: [_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { align: "start", gap: "lg", children: [_jsx(Div, { className: "shrink-0", children: _jsx(AvatarDisplay, { cropData: hasPending ? pendingCropData : cropData, size: "2xl", className: `border-4 ${hasPending ? "border-primary/60 ring-2 ring-primary/20" : "border-
|
|
112
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { align: "start", gap: "lg", children: [_jsx(Div, { className: "shrink-0", children: _jsx(AvatarDisplay, { cropData: hasPending ? pendingCropData : cropData, size: "2xl", className: `border-4 ${hasPending ? "border-primary/60 ring-2 ring-primary/20" : "border-[var(--appkit-color-border)]"}`, displayName: displayName }) }), _jsxs(Stack, { className: "flex-1", gap: "3", children: [_jsx(Text, { variant: "secondary", size: "xs", children: "JPG, PNG, WEBP or GIF up to 10MB." }), isUploading ? (_jsx(Progress, { value: 50, variant: "primary", size: "sm", label: t.uploading })) : null, hasPending && !isUploading ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Text, { variant: "secondary", className: "text-primary", size: "xs", children: t.readyToSave }), _jsxs(Row, { gap: "3", children: [_jsx(Button, { type: "button", onClick: handleConfirmSave, size: "sm", children: t.saveAvatar }), _jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: handleCancelPending, children: t.cancelChange })] })] })) : null, !hasPending && !isUploading ? (_jsxs(Row, { gap: "3", children: [_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), size: "sm", children: previewUrl ? t.changePhoto : t.chooseImage }), previewUrl ? (_jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: handleRemovePhoto, children: t.removePhoto })) : null] })) : null, _jsx(Input, { ref: fileInputRef, bare: true, type: "file", accept: "image/*", onChange: handleFileSelect, className: "hidden", "aria-label": t.changePhoto })] })] }), uploadApiError ? (_jsx(Alert, { variant: "error", children: uploadApiError.message })) : null] }), showCropModal && tempImageUrl ? (_jsx(ImageCropModal, { isOpen: showCropModal, imageUrl: tempImageUrl, onClose: handleCropCancel, onSave: handleCropSave, initialCropData: cropData || undefined })) : null] }));
|
|
113
113
|
}
|
|
114
114
|
export default AvatarUpload;
|
|
@@ -125,7 +125,7 @@ export function MediaPickerModal({ isOpen, onClose, onSelect, accept = "image/*"
|
|
|
125
125
|
"rounded-lg border overflow-hidden text-left transition p-0 h-auto",
|
|
126
126
|
isSelected
|
|
127
127
|
? "border-primary-500 ring-2 ring-primary-300"
|
|
128
|
-
: "border-
|
|
128
|
+
: "border-[var(--appkit-color-border)]",
|
|
129
129
|
].join(" "), "aria-pressed": isSelected, children: [_jsx(Row, { className: "aspect-square w-full", surface: "subtle", align: "center", justify: "center", children: isImage ? (
|
|
130
130
|
// eslint-disable-next-line @next/next/no-img-element
|
|
131
131
|
_jsx("img", { src: f.downloadURL, alt: f.name, className: "h-full w-full object-cover", loading: "lazy" })) : (_jsx(Text, { size: "sm", color: "muted", children: f.contentType ?? "file" })) }), _jsx(Text, { className: "block truncate p-1.5 font-mono", size: "xs", children: f.name.split("/").pop() })] }, f.name));
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
export function MediaSlider({ value, min = 0, max = 100, step = 1, onChange, onChangeEnd, className = "", }) {
|
|
3
3
|
return (_jsx("input", { type: "range", min: min, max: max, step: step, value: value, onChange: (e) => onChange?.(parseFloat(e.target.value)), onMouseUp: (e) => onChangeEnd?.(parseFloat(e.target.value)), onTouchEnd: (e) => onChangeEnd?.(parseFloat(e.target.value)), className: [
|
|
4
4
|
"w-full h-2 appearance-none cursor-pointer rounded-full",
|
|
5
|
-
"bg-zinc-200
|
|
5
|
+
"bg-zinc-200 bg-[var(--appkit-color-border)]",
|
|
6
6
|
"accent-primary-600 dark:accent-secondary-400",
|
|
7
7
|
className,
|
|
8
8
|
]
|
|
@@ -3,7 +3,18 @@ import type { MediaField } from "./types";
|
|
|
3
3
|
* Extract a storage object path from public/download URLs for the same bucket.
|
|
4
4
|
*/
|
|
5
5
|
export declare function extractStoragePathFromUrl(url: string, bucketName: string): string | null;
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Promote staged tmp/* uploads to canonical media/* path.
|
|
8
|
+
*
|
|
9
|
+
* Handles two URL formats:
|
|
10
|
+
* 1. `/media/{shortId}` — the new SEO-friendly short-ID format.
|
|
11
|
+
* Looks up the asset in the mediaAssets Firestore collection, moves the
|
|
12
|
+
* file from tmp/ → media/, updates the storagePath, and returns the same
|
|
13
|
+
* URL unchanged (the URL is stable across the move).
|
|
14
|
+
* 2. Raw GCS URLs (`https://storage.googleapis.com/...` or Firebase Storage URLs) —
|
|
15
|
+
* legacy path kept for backward compatibility; moves the file and returns a
|
|
16
|
+
* /media/{finalStoragePath} proxy URL.
|
|
17
|
+
*/
|
|
7
18
|
export declare function finalizeStagedMediaUrl(url: string): Promise<string>;
|
|
8
19
|
export declare function finalizeStagedMediaField(url: string | null | undefined): Promise<string | undefined>;
|
|
9
20
|
export declare function finalizeStagedMediaArray(urls: string[] | null | undefined): Promise<string[]>;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { getAdminStorage } from "../../providers/db-firebase";
|
|
2
2
|
import { FIREBASE_STORAGE_HOST, GCS_HOST } from "../../utils/media-url";
|
|
3
|
+
import { mediaAssetsRepository } from "./repository/media-assets.repository";
|
|
3
4
|
const TMP_MEDIA_PREFIX = "tmp/";
|
|
4
5
|
const FINAL_MEDIA_PREFIX = "media/";
|
|
6
|
+
const PROXY_URL_PREFIX = "/media/";
|
|
5
7
|
/**
|
|
6
8
|
* Extract a storage object path from public/download URLs for the same bucket.
|
|
7
9
|
*/
|
|
@@ -26,24 +28,75 @@ export function extractStoragePathFromUrl(url, bucketName) {
|
|
|
26
28
|
return null;
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Move a file from tmp/ to media/ in Firebase Storage.
|
|
33
|
+
* Returns the new storage path.
|
|
34
|
+
*/
|
|
35
|
+
async function moveToFinalPath(sourcePath) {
|
|
31
36
|
const storage = getAdminStorage();
|
|
32
37
|
const bucket = storage.bucket();
|
|
33
|
-
const sourcePath = extractStoragePathFromUrl(url, bucket.name);
|
|
34
|
-
if (!sourcePath || !sourcePath.startsWith(TMP_MEDIA_PREFIX)) {
|
|
35
|
-
return url;
|
|
36
|
-
}
|
|
37
38
|
const destinationPath = sourcePath.replace(TMP_MEDIA_PREFIX, FINAL_MEDIA_PREFIX);
|
|
38
|
-
if (destinationPath === sourcePath)
|
|
39
|
-
return
|
|
40
|
-
}
|
|
39
|
+
if (destinationPath === sourcePath)
|
|
40
|
+
return sourcePath;
|
|
41
41
|
const sourceFile = bucket.file(sourcePath);
|
|
42
42
|
const destinationFile = bucket.file(destinationPath);
|
|
43
43
|
await sourceFile.copy(destinationFile);
|
|
44
|
-
await destinationFile.makePublic();
|
|
45
44
|
await sourceFile.delete({ ignoreNotFound: true });
|
|
46
|
-
return
|
|
45
|
+
return destinationPath;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Promote staged tmp/* uploads to canonical media/* path.
|
|
49
|
+
*
|
|
50
|
+
* Handles two URL formats:
|
|
51
|
+
* 1. `/media/{shortId}` — the new SEO-friendly short-ID format.
|
|
52
|
+
* Looks up the asset in the mediaAssets Firestore collection, moves the
|
|
53
|
+
* file from tmp/ → media/, updates the storagePath, and returns the same
|
|
54
|
+
* URL unchanged (the URL is stable across the move).
|
|
55
|
+
* 2. Raw GCS URLs (`https://storage.googleapis.com/...` or Firebase Storage URLs) —
|
|
56
|
+
* legacy path kept for backward compatibility; moves the file and returns a
|
|
57
|
+
* /media/{finalStoragePath} proxy URL.
|
|
58
|
+
*/
|
|
59
|
+
export async function finalizeStagedMediaUrl(url) {
|
|
60
|
+
// ── Path 1: /media/{shortId} proxy URLs (new system) ──────────────────────
|
|
61
|
+
if (url.startsWith(PROXY_URL_PREFIX)) {
|
|
62
|
+
const afterPrefix = url.slice(PROXY_URL_PREFIX.length);
|
|
63
|
+
// Single-segment = shortId. Multi-segment = legacy raw path embedded in URL.
|
|
64
|
+
if (!afterPrefix.includes("/")) {
|
|
65
|
+
const shortId = afterPrefix;
|
|
66
|
+
try {
|
|
67
|
+
const asset = await mediaAssetsRepository.findById(shortId);
|
|
68
|
+
if (asset && asset.status === "staged" && asset.storagePath.startsWith(TMP_MEDIA_PREFIX)) {
|
|
69
|
+
const finalPath = await moveToFinalPath(asset.storagePath);
|
|
70
|
+
await mediaAssetsRepository.promoteToFinalized(shortId, finalPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Non-fatal: URL is still valid; proxy will serve whatever storagePath points to.
|
|
75
|
+
}
|
|
76
|
+
return url; // URL is stable — same before and after the file move
|
|
77
|
+
}
|
|
78
|
+
// Multi-segment legacy: /media/tmp/folder/uid/file.jpg → move + return proxy URL
|
|
79
|
+
const rawPath = afterPrefix;
|
|
80
|
+
if (rawPath.startsWith(TMP_MEDIA_PREFIX)) {
|
|
81
|
+
try {
|
|
82
|
+
const finalPath = await moveToFinalPath(rawPath);
|
|
83
|
+
return `${PROXY_URL_PREFIX}${finalPath}`;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return url;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return url; // already at media/ or other path — no-op
|
|
90
|
+
}
|
|
91
|
+
// ── Path 2: Legacy raw GCS URLs ────────────────────────────────────────────
|
|
92
|
+
const storage = getAdminStorage();
|
|
93
|
+
const bucket = storage.bucket();
|
|
94
|
+
const sourcePath = extractStoragePathFromUrl(url, bucket.name);
|
|
95
|
+
if (!sourcePath || !sourcePath.startsWith(TMP_MEDIA_PREFIX)) {
|
|
96
|
+
return url;
|
|
97
|
+
}
|
|
98
|
+
const finalPath = await moveToFinalPath(sourcePath);
|
|
99
|
+
return `${PROXY_URL_PREFIX}${finalPath}`;
|
|
47
100
|
}
|
|
48
101
|
export async function finalizeStagedMediaField(url) {
|
|
49
102
|
if (!url)
|
|
@@ -95,5 +95,5 @@ export function ImageCropModal({ isOpen, imageUrl, onClose, onSave, initialCropD
|
|
|
95
95
|
left: `${position.x}%`,
|
|
96
96
|
top: `${position.y}%`,
|
|
97
97
|
transform: "translate(-50%, -50%)",
|
|
98
|
-
} }), _jsxs(Div, { className: "absolute inset-0 pointer-events-none", children: [_jsx(Div, { layout: "grid", className: "absolute w-full h-full grid-cols-3 grid-rows-3", children: Array.from({ length: 9 }).map((_, i) => (_jsx(Div, { className: "border border-white/20
|
|
98
|
+
} }), _jsxs(Div, { className: "absolute inset-0 pointer-events-none", children: [_jsx(Div, { layout: "grid", className: "absolute w-full h-full grid-cols-3 grid-rows-3", children: Array.from({ length: 9 }).map((_, i) => (_jsx(Div, { className: "border border-white/20 border-[var(--appkit-color-border)]/30" }, i))) }), _jsxs(Row, { className: "absolute inset-0", align: "center", justify: "center", children: [_jsx(Div, { className: "w-6 h-px", surface: "default" }), _jsx(Div, { className: "absolute h-6 w-px", surface: "default" })] })] })] }), _jsxs(Stack, { gap: "xs", children: [_jsxs(Row, { justify: "between", children: [_jsx(Text, { size: "xs", weight: "medium", children: t("cropZoom") }), _jsxs(Text, { size: "xs", color: "muted", children: [Math.round(zoom * 100), "%"] })] }), _jsxs(Row, { gap: "3", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setZoom(Math.max(0.1, zoom - 0.1)), "aria-label": t("cropZoomOut"), children: _jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7" }) }) }), _jsx(MediaSlider, { value: zoom, min: 0.1, max: 3, step: 0.01, onChange: (v) => setZoom(v), className: "flex-1" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => setZoom(Math.min(3, zoom + 0.1)), "aria-label": t("cropZoomIn"), children: _jsx("svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" }) }) })] }), _jsx(Row, { gap: "sm", children: [0.5, 1, 1.5, 2].map((preset) => (_jsxs(Button, { rounded: "lg", type: "button", variant: "ghost", size: "sm", onClick: () => setZoom(preset), className: "px-3 py-1 text-xs bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)]", children: [preset * 100, "%"] }, preset))) })] }), showZoomWarning && (_jsxs(Alert, { variant: "warning", children: [_jsx(Span, { weight: "bold", children: t("cropWarningTitle") }), " ", t("cropWarningMessage")] })), _jsxs(Row, { textSize: "xs", justify: "between", className: "text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)]", children: [_jsxs(Span, { children: [t("cropPosition"), ": ", Math.round(position.x), "%, ", Math.round(position.y), "%"] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: handleReset, className: "text-primary-600 dark:text-primary-400 hover:underline", children: t("cropReset") })] }), _jsxs(Row, { gap: "sm", padding: "t-xs", children: [_jsx(Button, { onClick: handleSave, variant: "primary", className: "flex-1", children: t("cropSave") }), _jsx(Button, { onClick: onClose, variant: "secondary", className: "flex-1", children: t("cropCancel") })] })] }) }));
|
|
99
99
|
}
|
|
@@ -75,5 +75,5 @@ export function ImageEditor({ isOpen, imageUrl, onClose, onSave, aspectRatio, ou
|
|
|
75
75
|
{ label: "4:3", ratio: 4 / 3 },
|
|
76
76
|
{ label: "16:9", ratio: 16 / 9 },
|
|
77
77
|
{ label: "3:2", ratio: 3 / 2 },
|
|
78
|
-
].map((preset) => (_jsx(Button, { rounded: "lg", type: "button", variant: "ghost", size: "sm", onClick: () => applyAspectRatio(preset.ratio), className: "px-3 py-1 text-xs bg-
|
|
78
|
+
].map((preset) => (_jsx(Button, { rounded: "lg", type: "button", variant: "ghost", size: "sm", onClick: () => applyAspectRatio(preset.ratio), className: "px-3 py-1 text-xs bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)]", children: preset.label }, preset.label))) })), _jsxs(Row, { gap: "sm", padding: "t-xs", children: [_jsx(Button, { onClick: handleSave, variant: "primary", className: "flex-1", disabled: saving, children: saving ? t("cropSaving") : t("cropSave") }), _jsx(Button, { onClick: onClose, variant: "secondary", className: "flex-1", children: t("cropCancel") })] })] }) }));
|
|
79
79
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { BaseRepository } from "../../../providers/db-firebase";
|
|
2
|
+
export interface MediaAssetDocument {
|
|
3
|
+
/** SEO-friendly slug used in /media/{id} URLs (e.g. "product-image-charizard-1-20260805"). */
|
|
4
|
+
id: string;
|
|
5
|
+
/** Actual path inside the Firebase Storage bucket. Changes from tmp/ → media/ on finalize. */
|
|
6
|
+
storagePath: string;
|
|
7
|
+
/** Firebase Auth UID of the uploader. */
|
|
8
|
+
uploadedBy: string;
|
|
9
|
+
contentType: string;
|
|
10
|
+
/** File size in bytes. */
|
|
11
|
+
size: number;
|
|
12
|
+
isPublic: boolean;
|
|
13
|
+
/** staged = still in tmp/; finalized = moved to media/ by finalizeStagedMediaUrl. */
|
|
14
|
+
status: "staged" | "finalized";
|
|
15
|
+
createdAt: Date;
|
|
16
|
+
}
|
|
17
|
+
export type MediaAssetCreateInput = Omit<MediaAssetDocument, "id" | "createdAt">;
|
|
18
|
+
export declare class MediaAssetsRepository extends BaseRepository<MediaAssetDocument> {
|
|
19
|
+
constructor();
|
|
20
|
+
findByStoragePath(storagePath: string): Promise<MediaAssetDocument | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Create a new mediaAssets document with a collision-safe shortId.
|
|
23
|
+
* Tries `preferredShortId` first, then appends a random 4-char suffix up to
|
|
24
|
+
* MAX_ATTEMPTS times. Falls back to a base-36 timestamp suffix.
|
|
25
|
+
*/
|
|
26
|
+
createAsset(preferredShortId: string, data: MediaAssetCreateInput): Promise<MediaAssetDocument>;
|
|
27
|
+
/**
|
|
28
|
+
* Update storagePath + status when the file is promoted from tmp/ to media/.
|
|
29
|
+
* Called by finalizeStagedMediaUrl.
|
|
30
|
+
*/
|
|
31
|
+
promoteToFinalized(shortId: string, finalStoragePath: string): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export declare const mediaAssetsRepository: MediaAssetsRepository;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { BaseRepository } from "../../../providers/db-firebase";
|
|
2
|
+
function generateRand4() {
|
|
3
|
+
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
4
|
+
const indices = new Uint8Array(4);
|
|
5
|
+
globalThis.crypto.getRandomValues(indices);
|
|
6
|
+
return Array.from(indices, (i) => chars[i % chars.length]).join("");
|
|
7
|
+
}
|
|
8
|
+
export class MediaAssetsRepository extends BaseRepository {
|
|
9
|
+
constructor() {
|
|
10
|
+
super("mediaAssets");
|
|
11
|
+
}
|
|
12
|
+
async findByStoragePath(storagePath) {
|
|
13
|
+
return this.findOneBy("storagePath", storagePath);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Create a new mediaAssets document with a collision-safe shortId.
|
|
17
|
+
* Tries `preferredShortId` first, then appends a random 4-char suffix up to
|
|
18
|
+
* MAX_ATTEMPTS times. Falls back to a base-36 timestamp suffix.
|
|
19
|
+
*/
|
|
20
|
+
async createAsset(preferredShortId, data) {
|
|
21
|
+
const MAX_ATTEMPTS = 5;
|
|
22
|
+
for (let i = 0; i < MAX_ATTEMPTS; i++) {
|
|
23
|
+
const shortId = i === 0 ? preferredShortId : `${preferredShortId}-${generateRand4()}`;
|
|
24
|
+
const existing = await this.findById(shortId);
|
|
25
|
+
if (!existing) {
|
|
26
|
+
return this.createWithId(shortId, data);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const fallbackId = `${preferredShortId}-${Date.now().toString(36)}`;
|
|
30
|
+
return this.createWithId(fallbackId, data);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Update storagePath + status when the file is promoted from tmp/ to media/.
|
|
34
|
+
* Called by finalizeStagedMediaUrl.
|
|
35
|
+
*/
|
|
36
|
+
async promoteToFinalized(shortId, finalStoragePath) {
|
|
37
|
+
await this.update(shortId, {
|
|
38
|
+
storagePath: finalStoragePath,
|
|
39
|
+
status: "finalized",
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export const mediaAssetsRepository = new MediaAssetsRepository();
|
|
@@ -367,5 +367,5 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
|
|
|
367
367
|
return (_jsxs(Stack, { gap: "sm", children: [_jsx(Label, { className: "block", color: "muted", size: "sm", weight: "medium", children: label }), hasAlternateSources && !disabled && (_jsx(Row, { gap: "none", className: "appkit-media-upload__source-tabs", children: ["upload", ...(showYoutube ? ["youtube"] : []), ...(showExternal ? ["external"] : [])].map((tab) => (_jsx("button", { type: "button", onClick: () => setSourceTab(tab), className: [
|
|
368
368
|
"appkit-media-upload__source-tab",
|
|
369
369
|
sourceTab === tab ? "appkit-media-upload__source-tab--active" : "",
|
|
370
|
-
].filter(Boolean).join(" "), children: tab === "upload" ? "Upload" : tab === "youtube" ? "YouTube" : "External URL" }, tab))) })), hasAlternateSources && sourceTab === "youtube" && !disabled && (_jsx(YoutubeTabPanel, { ytInput: ytInput, setYtInput: setYtInput, ytError: ytError, setYtError: setYtError, onApply: handleYtApply, value: value })), hasAlternateSources && sourceTab === "external" && !disabled && (_jsx(ExternalUrlTabPanel, { extInput: extInput, setExtInput: setExtInput, extError: extError, setExtError: setExtError, onApply: handleExtApply })), (!hasAlternateSources || sourceTab === "upload") && value && !isLoading && (_jsx(MediaPreviewPanel, { value: value, label: label, disabled: disabled, enableTrim: enableTrim, enableThumbnail: enableThumbnail, onEditVideo: handleEditVideo, onRemove: handleRemove, tUpload: tUpload, tMediaEditor: tMediaEditor })), !disabled && !isLoading && (!hasAlternateSources || sourceTab === "upload") && (_jsxs(_Fragment, { children: [captureSource === "both" && isCameraSupported && (_jsxs(Row, { gap: "sm", children: [_jsx(Button, { type: "button", variant: inputMode === "file" ? "primary" : "outline", size: "sm", onClick: () => setInputMode("file"), children: t("switchToUpload") }), _jsx(Button, { type: "button", variant: inputMode === "camera" ? "primary" : "outline", size: "sm", onClick: () => setInputMode("camera"), children: t("switchToCamera") })] })), showCamera && isCameraSupported && (_jsx(CameraCapture, { mode: captureMode, facingMode: "environment", onCapture: handleCameraCapture, onError: (msg) => setError(msg) })), showCamera && !isCameraSupported && (_jsx(Button, { rounded: "xl", type: "button", onClick: () => mobileCaptureRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-
|
|
370
|
+
].filter(Boolean).join(" "), children: tab === "upload" ? "Upload" : tab === "youtube" ? "YouTube" : "External URL" }, tab))) })), hasAlternateSources && sourceTab === "youtube" && !disabled && (_jsx(YoutubeTabPanel, { ytInput: ytInput, setYtInput: setYtInput, ytError: ytError, setYtError: setYtError, onApply: handleYtApply, value: value })), hasAlternateSources && sourceTab === "external" && !disabled && (_jsx(ExternalUrlTabPanel, { extInput: extInput, setExtInput: setExtInput, extError: extError, setExtError: setExtError, onApply: handleExtApply })), (!hasAlternateSources || sourceTab === "upload") && value && !isLoading && (_jsx(MediaPreviewPanel, { value: value, label: label, disabled: disabled, enableTrim: enableTrim, enableThumbnail: enableThumbnail, onEditVideo: handleEditVideo, onRemove: handleRemove, tUpload: tUpload, tMediaEditor: tMediaEditor })), !disabled && !isLoading && (!hasAlternateSources || sourceTab === "upload") && (_jsxs(_Fragment, { children: [captureSource === "both" && isCameraSupported && (_jsxs(Row, { gap: "sm", children: [_jsx(Button, { type: "button", variant: inputMode === "file" ? "primary" : "outline", size: "sm", onClick: () => setInputMode("file"), children: t("switchToUpload") }), _jsx(Button, { type: "button", variant: inputMode === "camera" ? "primary" : "outline", size: "sm", onClick: () => setInputMode("camera"), children: t("switchToCamera") })] })), showCamera && isCameraSupported && (_jsx(CameraCapture, { mode: captureMode, facingMode: "environment", onCapture: handleCameraCapture, onError: (msg) => setError(msg) })), showCamera && !isCameraSupported && (_jsx(Button, { rounded: "xl", type: "button", onClick: () => mobileCaptureRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-[var(--appkit-color-border)] text-sm text-[var(--appkit-color-text-muted)]", children: t("switchToCamera") })), showFileInput && (_jsx(Button, { rounded: "xl", type: "button", onClick: () => fileInputRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-[var(--appkit-color-border)] text-sm text-[var(--appkit-color-text-muted)]", children: value ? tUpload("replaceFile") : tUpload("chooseFile") }))] })), isLoading && (_jsxs(Row, { gap: "sm", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { size: "sm", variant: "secondary", children: tUpload("uploading") })] })), _jsx("input", { ref: fileInputRef, type: "file", accept: accept, multiple: multiple, onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-input" }), showCamera && !isCameraSupported && (_jsx("input", { ref: mobileCaptureRef, type: "file", accept: captureModeAccept, capture: "environment", onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-capture-input" })), helperText && !error && (_jsx(Text, { variant: "secondary", size: "xs", children: helperText })), error && _jsx(Alert, { variant: "error", children: error }), pendingVideoUrl && (_jsx(VideoTrimModal, { isOpen: showTrimModal, videoUrl: pendingVideoUrl, onClose: handleTrimClose, onSave: handleTrimSave })), pendingVideoUrl && (_jsx(VideoThumbnailSelector, { isOpen: showThumbnailModal, videoUrl: pendingVideoUrl, onClose: handleThumbnailClose, onSelect: handleThumbnailSelect, onUpload: onUpload }))] }));
|
|
371
371
|
}
|
|
@@ -13,6 +13,7 @@ import { useCallback, useEffect, useState } from "react";
|
|
|
13
13
|
import { getClientRealtimeProvider } from "../../../contracts/client-realtime";
|
|
14
14
|
import { conversationPingPath, userConversationsPingPath, } from "../realtime";
|
|
15
15
|
import { normalizeError } from "../../../errors/normalize";
|
|
16
|
+
import { apiClient, ApiClientError } from "../../../http/ApiClient";
|
|
16
17
|
/**
|
|
17
18
|
* @deprecated Re-exported for backwards compat — import `conversationPingPath`
|
|
18
19
|
* from `@mohasinac/appkit` instead.
|
|
@@ -27,13 +28,14 @@ const DETAIL_ENDPOINT = (id) => `/api/user/conversations/${encodeURIComponent(id
|
|
|
27
28
|
const SEND_ENDPOINT = (id) => `/api/user/conversations/${encodeURIComponent(id)}/messages`;
|
|
28
29
|
const READ_ENDPOINT = (id) => `/api/user/conversations/${encodeURIComponent(id)}/read`;
|
|
29
30
|
async function fetchDetail(id) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
try {
|
|
32
|
+
return await apiClient.get(DETAIL_ENDPOINT(id));
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
if (err instanceof ApiClientError && err.status === 404)
|
|
36
|
+
return null;
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
37
39
|
}
|
|
38
40
|
export function useConversation(conversationId) {
|
|
39
41
|
const [conversation, setConversation] = useState(null);
|
|
@@ -88,15 +90,7 @@ export function useConversation(conversationId) {
|
|
|
88
90
|
const trimmed = body.trim();
|
|
89
91
|
if (!trimmed)
|
|
90
92
|
return;
|
|
91
|
-
|
|
92
|
-
method: "POST",
|
|
93
|
-
credentials: "include",
|
|
94
|
-
headers: { "Content-Type": "application/json" },
|
|
95
|
-
body: JSON.stringify({ body: trimmed }),
|
|
96
|
-
});
|
|
97
|
-
if (!res.ok) {
|
|
98
|
-
throw new Error(`Failed to send message (${res.status})`);
|
|
99
|
-
}
|
|
93
|
+
await apiClient.post(SEND_ENDPOINT(conversationId), { body: trimmed });
|
|
100
94
|
// RTDB ping will trigger refetch — but kick off one immediately in case
|
|
101
95
|
// the ping path isn't subscribed (e.g. provider not registered).
|
|
102
96
|
await refetch();
|
|
@@ -104,7 +98,8 @@ export function useConversation(conversationId) {
|
|
|
104
98
|
const markRead = useCallback(() => {
|
|
105
99
|
if (!conversationId)
|
|
106
100
|
return Promise.resolve();
|
|
107
|
-
return
|
|
101
|
+
return apiClient
|
|
102
|
+
.post(READ_ENDPOINT(conversationId))
|
|
108
103
|
.then(() => void refetch())
|
|
109
104
|
.catch((err) => void normalizeError(err));
|
|
110
105
|
}, [conversationId, refetch]);
|
|
@@ -8,13 +8,11 @@ import { useCallback, useEffect, useState } from "react";
|
|
|
8
8
|
import { getClientRealtimeProvider } from "../../../contracts/client-realtime";
|
|
9
9
|
import { userConversationsPingPath } from "../realtime";
|
|
10
10
|
import { normalizeError } from "../../../errors/normalize";
|
|
11
|
+
import { apiClient } from "../../../http/ApiClient";
|
|
11
12
|
const LIST_ENDPOINT = "/api/user/conversations";
|
|
12
13
|
async function fetchList() {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
throw new Error(`Failed to load conversations (${res.status})`);
|
|
16
|
-
const json = (await res.json());
|
|
17
|
-
return json.data?.items ?? [];
|
|
14
|
+
const data = await apiClient.get(LIST_ENDPOINT);
|
|
15
|
+
return data.items ?? [];
|
|
18
16
|
}
|
|
19
17
|
export function useConversations(userId) {
|
|
20
18
|
const [conversations, setConversations] = useState([]);
|
|
@@ -48,7 +48,7 @@ export function MarketplaceOrderCard({ order, links, labels, className = "", var
|
|
|
48
48
|
const longPress = useLongPress(() => onSelect?.(orderId, !isSelected));
|
|
49
49
|
const isListVariant = variant === "list";
|
|
50
50
|
const itemCount = order.items?.length ?? 0;
|
|
51
|
-
return (_jsxs("div", { className: `group relative overflow-hidden rounded-xl border border-zinc-200 bg-
|
|
51
|
+
return (_jsxs("div", { className: `group relative overflow-hidden rounded-xl border border-zinc-200 bg-[var(--appkit-color-surface)] transition-all duration-200 hover:shadow-md border-[var(--appkit-color-border-subtle)] bg-[var(--appkit-color-surface)] ${isSelected ? "ring-2 ring-primary-500 dark:ring-primary-400" : ""} ${className}`, onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, "data-section": "marketplaceordercard-div-411", children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: handleSelect, label: isSelected ? "Deselect order" : "Select order", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100 transition-opacity" })), _jsxs("div", { className: `flex gap-4 p-4 ${isListVariant ? "flex-row items-center justify-between" : "flex-col"}`, "data-section": "marketplaceordercard-div-412", children: [_jsxs("div", { className: `flex items-start gap-3 ${selectable ? "pl-8" : ""} ${isListVariant ? "min-w-0 flex-1" : ""}`, "data-section": "marketplaceordercard-div-413", children: [_jsx(Row, { className: "h-10 w-10 flex-shrink-0", surface: "subtle", align: "center", justify: "center", rounded: "lg", children: _jsx(Package, { className: "h-5 w-5 text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)]" }) }), _jsxs(Stack, { className: "min-w-0", gap: "xs", children: [_jsx(TextLink, { href: detailHref, className: "leading-tight", children: _jsx(Text, { weight: "semibold", className: "truncate", children: order.productTitle ?? shortId }) }), _jsxs(Caption, { className: "font-mono", children: [mergedLabels.orderNumber, " ", shortId] }), _jsxs(Caption, { children: [mergedLabels.placedOn, " ", formatDate(order.orderDate)] }), itemCount > 1 && (_jsxs(Caption, { children: [mergedLabels.items, ": ", _jsx(Span, { weight: "medium", children: itemCount })] }))] })] }), _jsxs("div", { className: `flex ${isListVariant ? "flex-shrink-0 items-center gap-4" : "flex-col gap-3"}`, "data-section": "marketplaceordercard-div-416", children: [_jsxs("div", { className: `flex ${isListVariant ? "items-center gap-4" : "flex-wrap items-center justify-between gap-3"}`, "data-section": "marketplaceordercard-div-417", children: [_jsx(StatusBadge, { status: status, label: statusLabel }), _jsx(Text, { weight: "semibold", className: "tabular-nums", children: formatCurrency(order.totalPrice, order.currency) })] }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [(isShipped || isDelivered) &&
|
|
52
52
|
trackHref &&
|
|
53
53
|
order.trackingNumber && (_jsx(Button, { variant: "ghost", size: "sm", className: CLS_ACTION_BUTTON, onClick: () => navigate(trackHref), children: mergedLabels.trackOrder })), isDelivered && reviewHref && (_jsx(Button, { variant: "outline", size: "sm", className: CLS_ACTION_BUTTON, onClick: () => navigate(reviewHref), children: mergedLabels.writeReview })), _jsx(Button, { variant: "secondary", size: "sm", className: CLS_ACTION_BUTTON, onClick: () => navigate(detailHref), children: mergedLabels.viewOrder })] })] })] })] }));
|
|
54
54
|
}
|
|
@@ -12,7 +12,7 @@ const STATUS_COLORS = {
|
|
|
12
12
|
cancelled: "bg-error-surface text-error",
|
|
13
13
|
refunded: "bg-warning-surface text-warning",
|
|
14
14
|
return_requested: "bg-warning-surface text-warning",
|
|
15
|
-
returned: "bg-
|
|
15
|
+
returned: "bg-[var(--appkit-color-surface)] text-[var(--appkit-color-text-muted)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
|
|
16
16
|
};
|
|
17
17
|
export function OrderCard({ order, onClick, labels = {} }) {
|
|
18
18
|
const date = order.createdAt
|
|
@@ -22,7 +22,7 @@ export function OrderCard({ order, onClick, labels = {} }) {
|
|
|
22
22
|
day: "numeric",
|
|
23
23
|
})
|
|
24
24
|
: "";
|
|
25
|
-
const statusColor = STATUS_COLORS[order.orderStatus] ?? "bg-
|
|
25
|
+
const statusColor = STATUS_COLORS[order.orderStatus] ?? "bg-[var(--appkit-color-surface)] text-neutral-700 bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]";
|
|
26
26
|
// SB8-F — count unrevealed prize-draw entries to surface a "reveals pending" badge.
|
|
27
27
|
const unrevealedPrizeDraws = order.items.filter((it) => it.listingType === "prize-draw" &&
|
|
28
28
|
it.prizeRevealStatus !== undefined &&
|
|
@@ -34,7 +34,7 @@ export function OrderCard({ order, onClick, labels = {} }) {
|
|
|
34
34
|
.sort()[0];
|
|
35
35
|
return (_jsxs(Div, { padding: "5", role: onClick ? "button" : undefined, tabIndex: onClick ? 0 : undefined, onKeyDown: onClick
|
|
36
36
|
? (e) => (e.key === "Enter" || e.key === " ") && onClick(order)
|
|
37
|
-
: undefined, onClick: onClick ? () => onClick(order) : undefined, className: `border border-neutral-200 ${onClick ? "cursor-pointer transition hover:shadow-md" : ""}`, rounded: "xl", surface: "default", children: [_jsxs(Row, { wrap: true, align: "start", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsxs(Text, { className: "text-
|
|
37
|
+
: undefined, onClick: onClick ? () => onClick(order) : undefined, className: `border border-neutral-200 ${onClick ? "cursor-pointer transition hover:shadow-md" : ""}`, rounded: "xl", surface: "default", children: [_jsxs(Row, { wrap: true, align: "start", justify: "between", gap: "3", children: [_jsxs(Div, { children: [_jsxs(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: ["Order #", order.id.slice(-8).toUpperCase()] }), date && (_jsx(Text, { className: "mt-0.5", color: "faint", size: "xs", children: date }))] }), _jsx(Span, { size: "xs", weight: "semibold", className: `capitalize ${statusColor}`, rounded: "full", padding: "pill-md", children: labels[order.orderStatus] ?? order.orderStatus.replace(/_/g, " ") })] }), revealsRemaining > 0 && (_jsxs(Row, { gap: "sm", className: "mt-2", children: [_jsxs(Span, { size: "xs", weight: "semibold", className: CLS_PRIZE_BADGE, children: [revealsRemaining, " ", revealsRemaining === 1 ? "reveal" : "reveals", " pending"] }), earliestDeadline && (_jsxs(Span, { size: "xs", color: "muted", children: ["before ", new Date(earliestDeadline).toLocaleDateString(getDefaultLocale(), { month: "short", day: "numeric" })] }))] })), _jsxs(Row, { wrap: true, gap: "3", className: "mt-4", children: [order.items.slice(0, 3).map((item, i) => (_jsxs(Row, { gap: "sm", children: [item.image && (_jsx("img", { src: item.image, alt: item.title, className: "h-10 w-10 object-cover rounded-lg" })), _jsxs(Div, { children: [_jsx(Text, { truncate: true, size: "sm", weight: "medium", color: "primary", children: item.title }), _jsxs(Text, { size: "xs", color: "faint", children: ["\u00D7", item.quantity] })] })] }, i))), order.items.length > 3 && (_jsxs(Span, { size: "xs", className: "self-center", color: "faint", children: ["+", order.items.length - 3, " more"] }))] }), _jsxs(Row, { justify: "between", className: "mt-4 border-t border-neutral-100", padding: "t-sm", children: [_jsxs(Span, { size: "sm", className: "text-[var(--appkit-color-text-muted)]", children: [order.currency ?? "", " Total"] }), _jsx(Span, { weight: "semibold", className: "text-[var(--appkit-color-text)]", children: formatCurrency(order.total, order.currency) })] })] }));
|
|
38
38
|
}
|
|
39
39
|
export function OrdersList({ orders, isLoading, onOrderClick, totalPages = 1, currentPage = 1, onPageChange, emptyLabel = "No orders found", }) {
|
|
40
40
|
if (isLoading) {
|
|
@@ -62,5 +62,5 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
|
|
|
62
62
|
/* ── Compact list layout ── */
|
|
63
63
|
_jsxs(_Fragment, { children: [_jsxs(Row, { className: "min-w-0", align: "start", justify: "between", gap: "sm", children: [_jsx(TextLink, { href: detailHref, className: "min-w-0 flex-1", children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), wishlistActions && (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: `shrink-0 p-[0.25rem] leading-none ${inWishlist ? "text-primary" : "text-[var(--appkit-color-text-faint)]"}`, onClick: handleWishlist, "aria-label": inWishlist ? mergedLabels.removeFromWishlist : mergedLabels.addToWishlist, children: inWishlist ? "♥" : "♡" }))] }), _jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [_jsx(Text, { className: "text-primary", size: "sm", weight: "bold", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "self-start mt-0.5", onClick: handleNavigate, children: mergedLabels.reserveNow })] })) : (
|
|
64
64
|
/* ── Full grid layout ── */
|
|
65
|
-
_jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-xs text-
|
|
65
|
+
_jsxs(_Fragment, { children: [_jsx(TextLink, { href: detailHref, children: _jsx(Text, { size: "sm", weight: "medium", color: "primary", truncate: 2, children: product.title }) }), product.description ? (_jsx(RichText, { html: normalizeRichTextHtml(product.description), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "line-clamp-2 text-xs text-[var(--appkit-color-text-muted)]" })) : null, _jsxs(Row, { justify: "between", className: "mt-1", gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: formatCurrency(product.price, getDefaultCurrency()) }), shipDate && _jsx(PreorderBadge, { shipDate: shipDate })] }), _jsxs(Row, { justify: "between", className: "mt-2", gap: "sm", children: [_jsx(Button, { type: "button", variant: "primary", size: "sm", textSize: "xs", className: "flex-1", onClick: handleNavigate, children: mergedLabels.reserveNow }), onAddToCart ? (_jsx(Button, { type: "button", variant: "outline", size: "sm", textSize: "xs", className: "flex-1", onClick: (e) => { e.stopPropagation(); onAddToCart(product); }, children: mergedLabels.addToCart })) : null, wishlistActions ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", className: `${inWishlist ? "text-primary" : "text-[var(--appkit-color-text-muted)]"}`, onClick: handleWishlist, "aria-label": inWishlist ? mergedLabels.removeFromWishlist : mergedLabels.addToWishlist, children: inWishlist ? "♥" : "♡" })) : null] })] })) })] }));
|
|
66
66
|
}
|
|
@@ -5,7 +5,7 @@ const STATUS_MAP = {
|
|
|
5
5
|
pending: { label: "Pending", color: "bg-warning-surface text-warning" },
|
|
6
6
|
confirmed: { label: "Confirmed", color: "bg-info-surface text-info" },
|
|
7
7
|
ready: { label: "Ready", color: "bg-success-surface text-success" },
|
|
8
|
-
fulfilled: { label: "Fulfilled", color: "bg-
|
|
8
|
+
fulfilled: { label: "Fulfilled", color: "bg-[var(--appkit-color-surface)] text-gray-700 bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]" },
|
|
9
9
|
cancelled: { label: "Cancelled", color: "bg-error-surface text-error" },
|
|
10
10
|
};
|
|
11
11
|
export function PreOrderBadge({ status, className = "" }) {
|
|
@@ -56,7 +56,7 @@ function PreOrderInfoSection({ title, productionStatus, maxPerUser, deliveryDate
|
|
|
56
56
|
codAvailable: "Cash on Delivery",
|
|
57
57
|
wishlistCount: (n) => `${n} wishlisted`,
|
|
58
58
|
categoryProductCount: (n, cat) => `${n} in ${cat}`,
|
|
59
|
-
} }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-
|
|
59
|
+
} }), (categoryName || category || brand) && (_jsxs(Row, { gap: "sm", wrap: true, children: [category && (_jsx(Link, { href: String(ROUTES.PUBLIC.CATEGORY_DETAIL(category)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-2.5 py-1 text-xs font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: categoryName || category })), !category && categoryName && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: categoryName })), brand && brandSlug && (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] px-2.5 py-1 text-xs font-medium text-[var(--appkit-color-text-muted)] transition-colors hover:border-primary-300 hover:bg-primary-50 hover:text-primary-700 dark:hover:border-primary-700/60 dark:hover:bg-primary-900/20 dark:hover:text-primary-400", children: brand })), brand && !brandSlug && (_jsx(Span, { layout: "inline-flex", size: "xs", weight: "medium", border: "subtle", rounded: "full", padding: "pill-sm-tall", surface: "muted", color: "muted", children: brand }))] })), productFeatures && features.length > 0 && (_jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures })), !productFeatures && features.length > 0 && (_jsxs(Div, { border: "subtle", surface: "muted", padding: "inline", rounded: "xl", children: [_jsx(Text, { className: "mb-2 tracking-wide", color: "muted", size: "xs", weight: "semibold", transform: "uppercase", children: "About this product" }), _jsx(Ul, { spacing: "comfortable", size: "sm", color: "primary", children: features.map((f, i) => (_jsxs(Li, { layout: "flex-start", gap: "2", children: [_jsx(Span, { className: "mt-0.5 flex-shrink-0 text-primary-500", children: "\u2022" }), f] }, i))) })] })), descriptionHtml && (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "text-sm leading-relaxed text-[var(--appkit-color-text-muted)] line-clamp-4" })), safeSeller && (_jsx(Div, { className: `${__P.p3}`, border: "subtle", rounded: "xl", surface: "muted", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Sold by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: safeSeller })] }), storeHref && (_jsx(Link, { href: storeHref, className: "shrink-0 rounded-lg bg-primary/10 dark:bg-primary/20 px-3 py-1.5 text-xs font-semibold text-primary-700 dark:text-primary-300 hover:bg-primary/20 dark:hover:bg-primary/30 transition-colors", children: "Visit Store \u2192" }))] }) }))] }));
|
|
60
60
|
}
|
|
61
61
|
function PreOrderBuyBarPanel({ reserveTarget, reservedCount, progressPct, productId, price, currency, depositAmount, depositPercent, isCancellable, tags, onReserveNow, }) {
|
|
62
62
|
return (_jsxs(Stack, { id: "pre-order-buy-bar", className: `${__P.p5}`, border: "subtle", gap: "md", rounded: "xl", surface: "muted", children: [reserveTarget > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Text, { size: "xs", color: "muted", children: [reservedCount, " of ", reserveTarget, " reserved"] }), _jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary-600 dark:text-primary-400", children: [progressPct, "%"] })] }), _jsx(Div, { className: `h-2 w-full ${__O.hidden}`, rounded: "full", surface: "subtle", children: _jsx(ProgressBarFill, { pct: progressPct, className: "h-full bg-primary transition-all rounded-full" }) })] })), onReserveNow ? (_jsx(PreOrderActionsClient, { productId: productId, price: price, currency: currency, depositAmount: depositAmount, depositPercent: depositPercent, isCancellable: isCancellable, tags: tags, onReserveNow: onReserveNow })) : (_jsxs(_Fragment, { children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5", color: "muted", size: "xs", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }))] })), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Reserve Now" }), isCancellable && (_jsx(Text, { size: "xs", align: "center", color: "muted", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) })), _jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "sm", justify: "center", className: "text-center", children: [
|
|
@@ -153,7 +153,7 @@ export async function PreOrderDetailPageView({ id, initialPreOrder, onReserveNow
|
|
|
153
153
|
? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
|
|
154
154
|
: undefined, renderGroupSection: groupId
|
|
155
155
|
? () => (_jsx(ShowGroupSection, { groupId: groupId, currentSlug: String(p.slug ?? product.id), isParent: isGroupParent, groupTitle: groupTitle }))
|
|
156
|
-
: undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-
|
|
156
|
+
: undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(Dl, { divide: "subtle", rounded: "xl", border: "subtle", className: "overflow-hidden", children: specs.map((s, i) => (_jsxs(Row, { gap: "md", oddEven: "zebra", surface: "default", padding: "inline", children: [_jsx(Dt, { className: "w-36 flex-shrink-0", color: "primary", weight: "medium", children: s.name }), _jsxs(Dd, { className: "flex-1", color: "muted", children: [s.value, s.unit ? ` ${s.unit}` : ""] })] }, i))) })) : undefined, customTabs: customSections.map((s) => ({
|
|
157
157
|
id: s.id,
|
|
158
158
|
label: s.title,
|
|
159
159
|
content: _jsx(CustomSectionTabContent, { section: s }),
|