@mohasinac/appkit 2.7.57 → 2.7.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/layout/DashboardLayoutClient.d.ts +7 -1
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -2
- package/dist/_internal/server/features/scams/og.js +1 -1
- package/dist/_internal/shared/actions/bulk-helpers.d.ts +3 -0
- package/dist/_internal/shared/actions/bulk-helpers.js +19 -0
- package/dist/_internal/shared/config/schema.d.ts +4 -4
- package/dist/client.d.ts +4 -1
- package/dist/client.js +3 -1
- package/dist/configs/next.js +11 -0
- package/dist/configs/tailwind.d.ts +16 -3
- package/dist/configs/tailwind.js +18 -5
- package/dist/features/account/components/AddressBook.js +1 -1
- package/dist/features/account/components/AddressesIndexListing.js +3 -4
- package/dist/features/account/components/BuyerAccountGuideView.js +7 -7
- package/dist/features/account/components/BuyerAuctionsGuideView.js +7 -7
- package/dist/features/account/components/BuyerOrdersGuideView.js +6 -6
- package/dist/features/account/components/BuyerShoppingGuideView.js +7 -7
- package/dist/features/account/components/UserOffersPanel.js +1 -1
- package/dist/features/account/components/UserSidebar.js +14 -10
- package/dist/features/account/components/UserSupportView.js +4 -4
- package/dist/features/admin/components/AdminAdsView.js +1 -1
- package/dist/features/admin/components/AdminAllEventEntriesView.js +4 -10
- package/dist/features/admin/components/AdminAnalyticsGuideView.js +3 -3
- package/dist/features/admin/components/AdminBidsView.js +3 -9
- package/dist/features/admin/components/AdminBlogView.js +4 -9
- package/dist/features/admin/components/AdminBrandEditorView.js +14 -17
- package/dist/features/admin/components/AdminBrandsView.js +4 -9
- package/dist/features/admin/components/AdminBundlesView.js +40 -59
- package/dist/features/admin/components/AdminCarouselView.js +8 -21
- package/dist/features/admin/components/AdminCartsView.js +3 -4
- package/dist/features/admin/components/AdminCatalogGuideView.js +5 -5
- package/dist/features/admin/components/AdminCategoriesView.js +4 -9
- package/dist/features/admin/components/AdminCategoryEditorView.js +1 -5
- package/dist/features/admin/components/AdminContactEditorView.js +2 -2
- package/dist/features/admin/components/AdminContactView.js +7 -25
- package/dist/features/admin/components/AdminContentGuideView.js +6 -6
- package/dist/features/admin/components/AdminCouponEditorView.js +14 -17
- package/dist/features/admin/components/AdminCouponsView.js +8 -16
- package/dist/features/admin/components/AdminFaqEditorView.js +14 -17
- package/dist/features/admin/components/AdminFaqsView.js +18 -69
- package/dist/features/admin/components/AdminFeatureEditorView.js +7 -9
- package/dist/features/admin/components/AdminFeatureFlagsView.js +1 -1
- package/dist/features/admin/components/AdminFeaturesView.js +1 -1
- package/dist/features/admin/components/AdminGuideHubView.js +1 -1
- package/dist/features/admin/components/AdminHistoryView.js +1 -1
- package/dist/features/admin/components/AdminListingScaffold.js +1 -1
- package/dist/features/admin/components/AdminNewsletterView.js +5 -15
- package/dist/features/admin/components/AdminNotificationsView.js +3 -9
- package/dist/features/admin/components/AdminOrdersGuideView.js +5 -5
- package/dist/features/admin/components/AdminOrdersView.js +3 -9
- package/dist/features/admin/components/AdminPayoutsView.js +3 -9
- package/dist/features/admin/components/AdminPrizeDrawsView.js +4 -9
- package/dist/features/admin/components/AdminProductEditorView.js +1 -5
- package/dist/features/admin/components/AdminProductsView.js +5 -10
- package/dist/features/admin/components/AdminReturnRequestsView.js +1 -1
- package/dist/features/admin/components/AdminReviewsView.js +3 -9
- package/dist/features/admin/components/AdminScammerEditorView.js +3 -3
- package/dist/features/admin/components/AdminScammersView.js +6 -12
- package/dist/features/admin/components/AdminSectionsView.js +1 -1
- package/dist/features/admin/components/AdminSessionsView.js +3 -9
- package/dist/features/admin/components/AdminSiteSettingsView.js +1 -1
- package/dist/features/admin/components/AdminStoreAddressesView.js +1 -1
- package/dist/features/admin/components/AdminStoresGuideView.js +6 -6
- package/dist/features/admin/components/AdminStoresView.js +3 -9
- package/dist/features/admin/components/AdminSublistingCategoriesView.js +3 -7
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +17 -20
- package/dist/features/admin/components/AdminSupportTicketDetailView.js +4 -4
- package/dist/features/admin/components/AdminSupportTicketsView.js +9 -15
- package/dist/features/admin/components/AdminTeamGuideView.js +4 -4
- package/dist/features/admin/components/AdminTeamView.js +6 -15
- package/dist/features/admin/components/AdminTrustGuideView.js +5 -5
- package/dist/features/admin/components/AdminUserEditorView.js +1 -1
- package/dist/features/admin/components/AdminUsersGuideView.js +5 -5
- package/dist/features/admin/components/AdminUsersView.js +3 -9
- package/dist/features/admin/components/AdminViewCards.js +1 -1
- package/dist/features/admin/components/AdminWishlistsView.js +1 -1
- package/dist/features/admin/components/DataTable.js +1 -1
- package/dist/features/admin/hooks/useAdminListing.d.ts +58 -0
- package/dist/features/admin/hooks/useAdminListing.js +71 -0
- package/dist/features/auctions/components/AuctionBidsTable.js +1 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +2 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +1 -1
- package/dist/features/auth/components/VerifyEmailView.js +1 -1
- package/dist/features/auth/schemas/firestore.d.ts +1 -0
- package/dist/features/blog/components/BlogIndexListing.js +3 -6
- package/dist/features/blog/components/BlogListView.js +1 -1
- package/dist/features/blog/components/BlogPostView.js +1 -1
- package/dist/features/cart/components/CartDrawer.js +1 -1
- package/dist/features/categories/components/CategoriesIndexListing.js +3 -6
- package/dist/features/categories/components/CategoryDetailPageView.js +1 -1
- package/dist/features/checkout/components/CheckoutStepper.js +1 -1
- package/dist/features/collections/components/CollectionCard.js +1 -1
- package/dist/features/copilot/components/AdminCopilotView.js +2 -2
- package/dist/features/events/components/AdminEventsView.js +4 -4
- package/dist/features/events/components/EventPollWidget.js +2 -2
- package/dist/features/events/components/EventStatusBadge.js +4 -4
- package/dist/features/events/components/EventsIndexListing.js +3 -4
- package/dist/features/events/components/SpinWheelView.js +1 -1
- package/dist/features/faq/components/FAQAccordion.js +2 -2
- package/dist/features/forms/Input.js +3 -3
- package/dist/features/forms/Select.js +2 -2
- package/dist/features/forms/Textarea.js +2 -2
- package/dist/features/forms/utils.d.ts +1 -1
- package/dist/features/forms/utils.js +1 -1
- package/dist/features/homepage/components/AnnouncementBar.d.ts +4 -1
- package/dist/features/homepage/components/AnnouncementBar.js +51 -4
- package/dist/features/homepage/components/CharacterHotspotForm.js +2 -2
- package/dist/features/homepage/components/MarketplaceHomepageView.d.ts +3 -1
- package/dist/features/homepage/components/MarketplaceHomepageView.js +2 -2
- package/dist/features/homepage/components/index.d.ts +1 -1
- package/dist/features/homepage/components/index.js +1 -1
- package/dist/features/homepage/lib/section-renderer.d.ts +2 -2
- package/dist/features/homepage/lib/section-renderer.js +2 -2
- package/dist/features/layout/BottomActions.js +2 -2
- package/dist/features/layout/ListingLayout.js +3 -3
- package/dist/features/layout/TitleBarLayout.js +1 -1
- package/dist/features/media/index.d.ts +2 -0
- package/dist/features/media/index.js +1 -0
- package/dist/features/media/modals/ImageCropModal.js +1 -1
- package/dist/features/media/modals/ImageEditor.d.ts +12 -0
- package/dist/features/media/modals/ImageEditor.js +73 -0
- package/dist/features/media/upload/ImageUpload.d.ts +5 -1
- package/dist/features/media/upload/ImageUpload.js +33 -8
- package/dist/features/media/upload/MediaUploadField.d.ts +5 -1
- package/dist/features/media/upload/MediaUploadField.js +26 -12
- package/dist/features/orders/components/OrdersList.js +7 -7
- package/dist/features/orders/components/RefundHistoryTable.js +2 -2
- package/dist/features/orders/components/RefundRequestView.js +3 -3
- package/dist/features/orders/repository/orders.repository.d.ts +0 -1
- package/dist/features/orders/repository/orders.repository.js +0 -1
- package/dist/features/orders/server.d.ts +1 -1
- package/dist/features/orders/server.js +1 -1
- package/dist/features/pre-orders/components/PreOrderActionsClient.js +1 -1
- package/dist/features/pre-orders/components/PreOrderBadge.js +4 -4
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
- package/dist/features/pre-orders/components/PreorderCard.js +2 -2
- package/dist/features/products/components/CustomSectionsEditor.js +1 -1
- package/dist/features/products/components/GroupSettingsPanel.js +64 -48
- package/dist/features/products/components/MakeOfferButton.js +2 -2
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/PrizeDrawItemsEditor.js +3 -3
- package/dist/features/products/components/PrizeRevealModal.js +3 -3
- package/dist/features/products/components/ProductDetailPageView.js +4 -4
- package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
- package/dist/features/products/components/ProductGrid.js +3 -3
- package/dist/features/products/repository/products.repository.d.ts +0 -2
- package/dist/features/products/repository/products.repository.js +0 -2
- package/dist/features/products/server.d.ts +1 -1
- package/dist/features/products/server.js +1 -1
- package/dist/features/promotions/components/CouponCard.js +7 -7
- package/dist/features/promotions/components/CouponsIndexListing.js +3 -2
- package/dist/features/promotions/components/PromotionsView.js +1 -1
- package/dist/features/reviews/components/ReviewDetailShell.js +2 -2
- package/dist/features/reviews/components/ReviewsIndexListing.js +3 -4
- package/dist/features/reviews/components/ReviewsList.js +1 -1
- package/dist/features/reviews/repository/reviews.repository.d.ts +0 -1
- package/dist/features/reviews/repository/reviews.repository.js +0 -1
- package/dist/features/reviews/server.d.ts +1 -1
- package/dist/features/reviews/server.js +1 -1
- package/dist/features/seller/components/SellerAddressesView.js +11 -21
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +2 -2
- package/dist/features/seller/components/SellerAuctionsView.js +14 -23
- package/dist/features/seller/components/SellerBidsView.js +6 -6
- package/dist/features/seller/components/SellerBundlesView.js +13 -21
- package/dist/features/seller/components/SellerClassifiedView.js +14 -22
- package/dist/features/seller/components/SellerCouponEditorView.js +2 -2
- package/dist/features/seller/components/SellerCouponsView.js +14 -20
- package/dist/features/seller/components/SellerDigitalCodesView.js +13 -21
- package/dist/features/seller/components/SellerFeaturesView.js +1 -1
- package/dist/features/seller/components/SellerGroupedListingsView.js +6 -10
- package/dist/features/seller/components/SellerLiveView.js +15 -23
- package/dist/features/seller/components/SellerOffersPanel.js +2 -2
- package/dist/features/seller/components/SellerOffersView.js +4 -5
- package/dist/features/seller/components/SellerOrdersView.js +9 -23
- package/dist/features/seller/components/SellerPayoutMethodsView.js +13 -25
- package/dist/features/seller/components/SellerPayoutsView.js +4 -5
- package/dist/features/seller/components/SellerPreOrdersView.js +13 -22
- package/dist/features/seller/components/SellerPrizeDrawsView.js +13 -22
- package/dist/features/seller/components/SellerProductsCards.js +1 -1
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +1 -1
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +3 -6
- package/dist/features/seller/components/SellerProductsView.js +10 -23
- package/dist/features/seller/components/SellerReviewsView.js +1 -1
- package/dist/features/seller/components/SellerShippingConfigsView.js +14 -26
- package/dist/features/seller/components/SellerStoreCategoriesView.js +16 -35
- package/dist/features/seller/components/SellerTemplatesView.js +27 -43
- package/dist/features/seller/components/analytics/SellerTopProducts.js +1 -1
- package/dist/features/stores/components/StoreFinanceGuideView.js +2 -2
- package/dist/features/stores/components/StoreHeader.js +1 -1
- package/dist/features/stores/components/StoreListingsGuideView.js +3 -3
- package/dist/features/stores/components/StoreOrdersGuideView.js +4 -4
- package/dist/features/stores/components/StoreReviewsListing.js +3 -4
- package/dist/features/stores/components/StoreSettingsGuideView.js +2 -2
- package/dist/features/stores/components/StoresIndexListing.js +3 -4
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +4 -4
- package/dist/features/wishlist/components/WishlistPage.js +1 -1
- package/dist/index.d.ts +4 -6
- package/dist/index.js +6 -12
- package/dist/react/contexts/SessionContext.d.ts +2 -0
- package/dist/react/contexts/SessionContext.js +2 -0
- package/dist/react/hooks/useEntityDelete.d.ts +15 -0
- package/dist/react/hooks/useEntityDelete.js +39 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +2 -0
- package/dist/repositories/index.d.ts +3 -3
- package/dist/repositories/index.js +3 -3
- package/dist/styles.css +286 -3
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/motion.d.ts +162 -0
- package/dist/tokens/motion.js +89 -0
- package/dist/ui/DataTable.js +3 -2
- package/dist/ui/components/Accordion.js +2 -1
- package/dist/ui/components/Card.d.ts +5 -2
- package/dist/ui/components/Card.js +29 -11
- package/dist/ui/components/Div.d.ts +2 -16
- package/dist/ui/components/Div.js +2 -16
- package/dist/ui/components/Layout.d.ts +9 -19
- package/dist/ui/components/Layout.js +9 -7
- package/dist/ui/components/ListingFilterDrawer.d.ts +10 -0
- package/dist/ui/components/ListingFilterDrawer.js +8 -0
- package/dist/ui/components/Modal.js +7 -4
- package/dist/ui/components/Motion.d.ts +75 -0
- package/dist/ui/components/Motion.js +122 -0
- package/dist/ui/components/Semantic.d.ts +81 -14
- package/dist/ui/components/Semantic.js +75 -10
- package/dist/ui/components/Semantic.style.css +261 -2
- package/dist/ui/components/SideDrawer.d.ts +1 -1
- package/dist/ui/components/SideDrawer.js +8 -6
- package/dist/ui/components/SideModal.d.ts +1 -1
- package/dist/ui/components/SideModal.js +11 -9
- package/dist/ui/components/StackedViewShell.js +6 -1
- package/dist/ui/components/Toast.js +5 -2
- package/dist/ui/components/Typography.d.ts +44 -3
- package/dist/ui/components/Typography.js +15 -30
- package/dist/ui/components/Typography.style.css +24 -0
- package/dist/ui/components/VacationBanner.js +2 -1
- package/dist/ui/components/surface-tokens.d.ts +70 -0
- package/dist/ui/components/surface-tokens.js +68 -0
- package/dist/ui/forms/FormShell.d.ts +6 -1
- package/dist/ui/forms/FormShell.js +32 -4
- package/dist/ui/index.d.ts +11 -2
- package/dist/ui/index.js +5 -2
- package/dist/validation/schemas.d.ts +28 -28
- package/package.json +3 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* ImageEditor — WhatsApp-style crop/rotate/zoom editor.
|
|
5
|
+
*
|
|
6
|
+
* Uses react-advanced-cropper for real pixel cropping (not just focal-point).
|
|
7
|
+
* Outputs a cropped File ready for upload.
|
|
8
|
+
*/
|
|
9
|
+
import { useState, useRef, useCallback } from "react";
|
|
10
|
+
import { useTranslations } from "next-intl";
|
|
11
|
+
import { Cropper } from "react-advanced-cropper";
|
|
12
|
+
import "react-advanced-cropper/dist/style.css";
|
|
13
|
+
import { Button, Div, Modal, Row, Text } from "../../../ui";
|
|
14
|
+
export function ImageEditor({ isOpen, imageUrl, onClose, onSave, aspectRatio, outputFormat = "image/webp", outputQuality = 0.9, maxOutputWidth = 2048, }) {
|
|
15
|
+
const t = useTranslations("mediaEditor");
|
|
16
|
+
const cropperRef = useRef(null);
|
|
17
|
+
const [rotation, setRotation] = useState(0);
|
|
18
|
+
const [saving, setSaving] = useState(false);
|
|
19
|
+
const handleRotate = useCallback((degrees) => {
|
|
20
|
+
setRotation((r) => (r + degrees) % 360);
|
|
21
|
+
cropperRef.current?.rotateImage(degrees);
|
|
22
|
+
}, []);
|
|
23
|
+
const handleFlipH = useCallback(() => {
|
|
24
|
+
cropperRef.current?.flipImage(true, false);
|
|
25
|
+
}, []);
|
|
26
|
+
const handleFlipV = useCallback(() => {
|
|
27
|
+
cropperRef.current?.flipImage(false, true);
|
|
28
|
+
}, []);
|
|
29
|
+
const handleReset = useCallback(() => {
|
|
30
|
+
setRotation(0);
|
|
31
|
+
cropperRef.current?.reset();
|
|
32
|
+
}, []);
|
|
33
|
+
const applyAspectRatio = useCallback((ratio) => {
|
|
34
|
+
if (!ratio) {
|
|
35
|
+
cropperRef.current?.reset();
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const coords = cropperRef.current?.getCoordinates();
|
|
39
|
+
if (!coords)
|
|
40
|
+
return;
|
|
41
|
+
cropperRef.current?.setCoordinates({ ...coords, height: coords.width / ratio });
|
|
42
|
+
}, []);
|
|
43
|
+
const handleSave = useCallback(async () => {
|
|
44
|
+
const cropper = cropperRef.current;
|
|
45
|
+
if (!cropper)
|
|
46
|
+
return;
|
|
47
|
+
setSaving(true);
|
|
48
|
+
try {
|
|
49
|
+
const canvas = cropper.getCanvas({
|
|
50
|
+
maxWidth: maxOutputWidth,
|
|
51
|
+
maxHeight: maxOutputWidth,
|
|
52
|
+
});
|
|
53
|
+
if (!canvas)
|
|
54
|
+
return;
|
|
55
|
+
const blob = await new Promise((resolve) => canvas.toBlob(resolve, outputFormat, outputQuality));
|
|
56
|
+
if (!blob)
|
|
57
|
+
return;
|
|
58
|
+
const ext = outputFormat.split("/")[1] || "webp";
|
|
59
|
+
const file = new File([blob], `cropped.${ext}`, { type: outputFormat });
|
|
60
|
+
onSave(file);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
setSaving(false);
|
|
64
|
+
}
|
|
65
|
+
}, [maxOutputWidth, outputFormat, outputQuality, onSave]);
|
|
66
|
+
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, title: t("cropTitle"), size: "lg", children: _jsxs(Div, { className: "space-y-3", children: [_jsx(Text, { variant: "secondary", size: "xs", children: t("cropInstruction") }), _jsx(Div, { className: "relative w-full max-h-[400px] overflow-hidden rounded-lg bg-zinc-100 dark:bg-slate-800", children: _jsx(Cropper, { ref: cropperRef, src: imageUrl, stencilProps: aspectRatio ? { aspectRatio } : undefined, className: "max-h-[400px]" }) }), _jsxs(Row, { gap: "sm", justify: "center", className: "flex-wrap", children: [_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => handleRotate(-90), "aria-label": t("cropRotateLeft"), children: _jsxs("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: "M3 10h10a4 4 0 014 4v0a4 4 0 01-4 4H3" }), _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M7 6l-4 4 4 4" })] }) }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => handleRotate(90), "aria-label": t("cropRotateRight"), children: _jsxs("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 10H11a4 4 0 00-4 4v0a4 4 0 004 4h10" }), _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M17 6l4 4-4 4" })] }) }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: handleFlipH, "aria-label": t("cropFlipH"), 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: "M7 16V4l-4 4m0 4l4 4M17 16V4l4 4m0 4l-4 4M12 2v20" }) }) }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: handleFlipV, "aria-label": t("cropFlipV"), children: _jsx("svg", { className: "w-4 h-4 rotate-90", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M7 16V4l-4 4m0 4l4 4M17 16V4l4 4m0 4l-4 4M12 2v20" }) }) }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: handleReset, children: t("cropReset") }), _jsxs(Text, { size: "xs", variant: "secondary", className: "ml-auto", children: [rotation, "\u00B0"] })] }), !aspectRatio && (_jsx(Row, { gap: "sm", justify: "center", className: "flex-wrap", children: [
|
|
67
|
+
{ label: "Free", ratio: undefined },
|
|
68
|
+
{ label: "1:1", ratio: 1 },
|
|
69
|
+
{ label: "4:3", ratio: 4 / 3 },
|
|
70
|
+
{ label: "16:9", ratio: 16 / 9 },
|
|
71
|
+
{ label: "3:2", ratio: 3 / 2 },
|
|
72
|
+
].map((preset) => (_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => applyAspectRatio(preset.ratio), className: "px-3 py-1 text-xs rounded-lg bg-zinc-100 dark:bg-slate-800", children: preset.label }, preset.label))) })), _jsxs(Div, { className: "flex gap-2 pt-2", 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") })] })] }) }));
|
|
73
|
+
}
|
|
@@ -9,6 +9,10 @@ export interface ImageUploadProps {
|
|
|
9
9
|
helperText?: string;
|
|
10
10
|
captureSource?: "file-only" | "camera-only" | "both";
|
|
11
11
|
enableCrop?: boolean;
|
|
12
|
+
/** Use the advanced pixel cropper instead of the focal-point editor. */
|
|
13
|
+
enableAdvancedCrop?: boolean;
|
|
14
|
+
/** Lock the crop aspect ratio (e.g. 1 for square, 16/9 for widescreen). */
|
|
15
|
+
cropAspectRatio?: number;
|
|
12
16
|
onCropDataChange?: (cropData: ImageCropData) => void;
|
|
13
17
|
}
|
|
14
|
-
export declare function ImageUpload({ currentImage, onUpload, onChange, accept, maxSizeMB, label, helperText, captureSource, enableCrop, onCropDataChange, }: ImageUploadProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export declare function ImageUpload({ currentImage, onUpload, onChange, accept, maxSizeMB, label, helperText, captureSource, enableCrop, enableAdvancedCrop, cropAspectRatio, onCropDataChange, }: ImageUploadProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -13,6 +13,7 @@ import { useCamera } from "../../../react";
|
|
|
13
13
|
import { Alert, Button, Div, Label, Progress, Row, Spinner, Text, } from "../../../ui";
|
|
14
14
|
import { MediaImage } from "../MediaImage";
|
|
15
15
|
import { ImageCropModal } from "../modals/ImageCropModal";
|
|
16
|
+
import { ImageEditor } from "../modals/ImageEditor";
|
|
16
17
|
import CameraCapture from "./CameraCapture";
|
|
17
18
|
function formatFileSize(bytes) {
|
|
18
19
|
if (bytes < 1024 * 1024)
|
|
@@ -30,12 +31,13 @@ function matchesMimeAccept(fileType, accept) {
|
|
|
30
31
|
return pSub === "*" || pSub === fSub;
|
|
31
32
|
});
|
|
32
33
|
}
|
|
33
|
-
export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/*", maxSizeMB = 10, label = "Upload Image", helperText, captureSource = "both", enableCrop = true, onCropDataChange, }) {
|
|
34
|
+
export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/*", maxSizeMB = 10, label = "Upload Image", helperText, captureSource = "both", enableCrop = true, enableAdvancedCrop = false, cropAspectRatio, onCropDataChange, }) {
|
|
34
35
|
const [uploading, setUploading] = useState(false);
|
|
35
36
|
const [preview, setPreview] = useState(currentImage || "");
|
|
36
37
|
const [error, setError] = useState("");
|
|
37
38
|
const [progress, setProgress] = useState(0);
|
|
38
39
|
const [showCropModal, setShowCropModal] = useState(false);
|
|
40
|
+
const [showAdvancedEditor, setShowAdvancedEditor] = useState(false);
|
|
39
41
|
const [cropPreviewUrl, setCropPreviewUrl] = useState(null);
|
|
40
42
|
const [pendingFile, setPendingFile] = useState(null);
|
|
41
43
|
const [captureMode, setCaptureMode] = useState("file");
|
|
@@ -71,7 +73,7 @@ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/
|
|
|
71
73
|
}
|
|
72
74
|
};
|
|
73
75
|
const prepareFile = (file) => {
|
|
74
|
-
if (!enableCrop) {
|
|
76
|
+
if (!enableCrop && !enableAdvancedCrop) {
|
|
75
77
|
void performUpload(file);
|
|
76
78
|
return;
|
|
77
79
|
}
|
|
@@ -80,7 +82,12 @@ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/
|
|
|
80
82
|
if (event.target?.result) {
|
|
81
83
|
setCropPreviewUrl(event.target.result);
|
|
82
84
|
setPendingFile(file);
|
|
83
|
-
|
|
85
|
+
if (enableAdvancedCrop) {
|
|
86
|
+
setShowAdvancedEditor(true);
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
setShowCropModal(true);
|
|
90
|
+
}
|
|
84
91
|
}
|
|
85
92
|
};
|
|
86
93
|
reader.readAsDataURL(file);
|
|
@@ -100,6 +107,27 @@ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/
|
|
|
100
107
|
setPendingFile(null);
|
|
101
108
|
}
|
|
102
109
|
};
|
|
110
|
+
const handleAdvancedEditorSave = (croppedFile) => {
|
|
111
|
+
setShowAdvancedEditor(false);
|
|
112
|
+
setCropPreviewUrl(null);
|
|
113
|
+
setPendingFile(null);
|
|
114
|
+
void performUpload(croppedFile);
|
|
115
|
+
};
|
|
116
|
+
const openEditor = (url) => {
|
|
117
|
+
setCropPreviewUrl(url);
|
|
118
|
+
if (enableAdvancedCrop)
|
|
119
|
+
setShowAdvancedEditor(true);
|
|
120
|
+
else
|
|
121
|
+
setShowCropModal(true);
|
|
122
|
+
};
|
|
123
|
+
const handleAdvancedEditorClose = () => {
|
|
124
|
+
setShowAdvancedEditor(false);
|
|
125
|
+
setCropPreviewUrl(null);
|
|
126
|
+
if (pendingFile) {
|
|
127
|
+
void performUpload(pendingFile);
|
|
128
|
+
setPendingFile(null);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
103
131
|
const handleCameraCapture = (blob) => {
|
|
104
132
|
// blob.type can be empty from some camera implementations; hard-coded
|
|
105
133
|
// image/webp is always correct here since ImageUpload is photo-only.
|
|
@@ -134,10 +162,7 @@ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/
|
|
|
134
162
|
fileInputRef.current.value = "";
|
|
135
163
|
onChange?.("");
|
|
136
164
|
};
|
|
137
|
-
return (_jsxs(Div, { className: "space-y-3", children: [label && (_jsx(Label, { className: "block text-sm font-medium text-zinc-600 dark:text-zinc-400", children: label })), _jsx(Div, { className: "relative", children: preview ? (_jsxs(Div, { className: "space-y-2", children: [_jsxs(Div, { className: "relative h-32 max-w-xs mx-auto overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700", children: [_jsx(MediaImage, { src: preview, alt: "Preview", size: "card" }), uploading && progress > 0 && (_jsx(Div, { className: "absolute inset-x-0 bottom-0", children: _jsx(Progress, { value: progress, size: "sm" }) }))] }), !uploading && (_jsxs(Row, { wrap: true, gap: "sm", children: [enableCrop && (_jsx(Button, { type: "button", onClick: () => {
|
|
138
|
-
setCropPreviewUrl(preview);
|
|
139
|
-
setShowCropModal(true);
|
|
140
|
-
}, variant: "secondary", size: "sm", children: tMediaEditor("editImage") })), _jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), variant: "outline", size: "sm", children: tUpload("change") }), _jsx(Button, { type: "button", onClick: handleRemove, variant: "danger", size: "sm", children: tUpload("remove") })] }))] })) : (_jsxs(_Fragment, { children: [showCamera && isCameraSupported && (_jsx(CameraCapture, { mode: "photo", facingMode: "environment", onCapture: handleCameraCapture, onError: setError })), !showCamera && (_jsxs(Div, { className: "space-y-2", children: [_jsxs(Row, { gap: "sm", className: "flex-wrap", children: [showFileInput && (_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), disabled: uploading, variant: "outline", size: "sm", children: uploading ? tUpload("uploading") : tUpload("clickToUpload") })), captureSource !== "file-only" && isCameraSupported && (_jsx(Button, { type: "button", onClick: () => setCaptureMode("camera"), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") })), captureSource !== "file-only" && !isCameraSupported && (_jsx(Button, { type: "button", onClick: () => mobileCaptureRef.current?.click(), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") }))] }), _jsxs(Text, { size: "xs", variant: "secondary", children: [accept === "image/*"
|
|
165
|
+
return (_jsxs(Div, { className: "space-y-3", children: [label && (_jsx(Label, { className: "block text-sm font-medium text-zinc-600 dark:text-zinc-400", children: label })), _jsx(Div, { className: "relative", children: preview ? (_jsxs(Div, { className: "space-y-2", children: [_jsxs(Div, { className: "relative h-32 max-w-xs mx-auto overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700", children: [_jsx(MediaImage, { src: preview, alt: "Preview", size: "card" }), uploading && progress > 0 && (_jsx(Div, { className: "absolute inset-x-0 bottom-0", children: _jsx(Progress, { value: progress, size: "sm" }) }))] }), !uploading && (_jsxs(Row, { wrap: true, gap: "sm", children: [(enableCrop || enableAdvancedCrop) && (_jsx(Button, { type: "button", onClick: () => openEditor(preview), variant: "secondary", size: "sm", children: tMediaEditor("editImage") })), _jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), variant: "outline", size: "sm", children: tUpload("change") }), _jsx(Button, { type: "button", onClick: handleRemove, variant: "danger", size: "sm", children: tUpload("remove") })] }))] })) : (_jsxs(_Fragment, { children: [showCamera && isCameraSupported && (_jsx(CameraCapture, { mode: "photo", facingMode: "environment", onCapture: handleCameraCapture, onError: setError })), !showCamera && (_jsxs(Div, { className: "space-y-2", children: [_jsxs(Row, { gap: "sm", className: "flex-wrap", children: [showFileInput && (_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), disabled: uploading, variant: "outline", size: "sm", children: uploading ? tUpload("uploading") : tUpload("clickToUpload") })), captureSource !== "file-only" && isCameraSupported && (_jsx(Button, { type: "button", onClick: () => setCaptureMode("camera"), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") })), captureSource !== "file-only" && !isCameraSupported && (_jsx(Button, { type: "button", onClick: () => mobileCaptureRef.current?.click(), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") }))] }), _jsxs(Text, { size: "xs", variant: "secondary", children: [accept === "image/*"
|
|
141
166
|
? `JPG PNG GIF WebP`
|
|
142
|
-
: accept.split(",").map((a) => a.split("/")[1]?.toUpperCase() ?? a).join(" "), " ", "\u2014 max ", maxSizeMB, "MB"] })] }))] })) }), _jsx("input", { ref: fileInputRef, type: "file", accept: accept, onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-input" }), showCamera && !isCameraSupported && (_jsx("input", { ref: mobileCaptureRef, type: "file", accept: "image/*", 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 }), uploading && (_jsxs(Row, { className: "gap-2", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { size: "sm", variant: "secondary", children: tUpload("uploadingProgress", { progress }) })] })), cropPreviewUrl && (_jsx(ImageCropModal, { isOpen: showCropModal, imageUrl: cropPreviewUrl, onClose: handleCropClose, onSave: handleCropSave }))] }));
|
|
167
|
+
: accept.split(",").map((a) => a.split("/")[1]?.toUpperCase() ?? a).join(" "), " ", "\u2014 max ", maxSizeMB, "MB"] })] }))] })) }), _jsx("input", { ref: fileInputRef, type: "file", accept: accept, onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-input" }), showCamera && !isCameraSupported && (_jsx("input", { ref: mobileCaptureRef, type: "file", accept: "image/*", 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 }), uploading && (_jsxs(Row, { className: "gap-2", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { size: "sm", variant: "secondary", children: tUpload("uploadingProgress", { progress }) })] })), cropPreviewUrl && !enableAdvancedCrop && (_jsx(ImageCropModal, { isOpen: showCropModal, imageUrl: cropPreviewUrl, onClose: handleCropClose, onSave: handleCropSave })), cropPreviewUrl && enableAdvancedCrop && (_jsx(ImageEditor, { isOpen: showAdvancedEditor, imageUrl: cropPreviewUrl, onClose: handleAdvancedEditorClose, onSave: handleAdvancedEditorSave, aspectRatio: cropAspectRatio }))] }));
|
|
143
168
|
}
|
|
@@ -45,5 +45,9 @@ export interface MediaUploadFieldProps {
|
|
|
45
45
|
* Set true after successful save to prevent auto-cleanup on unmount.
|
|
46
46
|
*/
|
|
47
47
|
isPersisted?: boolean;
|
|
48
|
+
/** Allow selecting multiple files at once. Each is uploaded individually. */
|
|
49
|
+
multiple?: boolean;
|
|
50
|
+
/** Called with each successfully uploaded URL when `multiple` is true. */
|
|
51
|
+
onAddUrl?: (url: string) => void;
|
|
48
52
|
}
|
|
49
|
-
export declare function MediaUploadField({ label, value, onChange, onChangeField, onUpload, kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled, helperText, captureSource, captureMode, enableTrim, enableThumbnail, onThumbnailChange, showYoutube, showExternal, onAbort, onStagedUrlsChange, isPersisted, }: MediaUploadFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
53
|
+
export declare function MediaUploadField({ label, value, onChange, onChangeField, onUpload, kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled, helperText, captureSource, captureMode, enableTrim, enableThumbnail, onThumbnailChange, showYoutube, showExternal, onAbort, onStagedUrlsChange, isPersisted, multiple, onAddUrl, }: MediaUploadFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -72,13 +72,13 @@ function ExternalUrlTabPanel({ extInput, setExtInput, extError, setExtError, onA
|
|
|
72
72
|
return (_jsxs(Div, { className: "space-y-2", children: [_jsxs(Row, { gap: "sm", children: [_jsx("input", { type: "url", value: extInput, onChange: (e) => { setExtInput(e.target.value); setExtError(""); }, placeholder: "https://example.com/image.jpg", className: "appkit-input flex-1" }), _jsx(Button, { type: "button", variant: "primary", size: "sm", onClick: onApply, children: "Apply" })] }), extError && _jsx(Alert, { variant: "error", children: extError }), _jsx(Text, { size: "xs", variant: "secondary", children: "External URLs are stored as-is and are not watermarked." })] }));
|
|
73
73
|
}
|
|
74
74
|
function MediaPreviewPanel({ value, label, disabled, enableTrim, enableThumbnail, onEditVideo, onRemove, tUpload, tMediaEditor, }) {
|
|
75
|
-
return (_jsxs(Div, { className: "rounded-lg border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900 p-3", children: [isVideo(value) ? (_jsx(Div, { className: "relative aspect-video overflow-hidden rounded-lg", children: _jsx(MediaVideo, { src: value, alt: label, controls: true, objectFit: "contain" }) })) : isImage(value) ? (_jsx(Div, { className: "relative aspect-video overflow-hidden rounded-lg", children: _jsx(MediaImage, { src: value, alt: label, size: "card", objectFit: "contain" }) })) : isPdf(value) ? (_jsxs(Row, { gap: "md", align: "center", children: [_jsx(Div, { "aria-hidden": true, className: "flex h-
|
|
75
|
+
return (_jsxs(Div, { className: "rounded-lg border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900 p-3", children: [isVideo(value) ? (_jsxs(Div, { className: "space-y-2", children: [_jsx(Div, { className: "relative aspect-video overflow-hidden rounded-lg", children: _jsx(MediaVideo, { src: value, alt: label, controls: true, objectFit: "contain" }) }), _jsx(Text, { size: "xs", variant: "secondary", children: "Video uploaded successfully. Large videos may take longer to process." })] })) : isImage(value) ? (_jsx(Div, { className: "relative aspect-video overflow-hidden rounded-lg", children: _jsx(MediaImage, { src: value, alt: label, size: "card", objectFit: "contain" }) })) : isPdf(value) ? (_jsxs(Div, { className: "space-y-2", children: [_jsx(Div, { className: "relative w-full overflow-hidden rounded-lg border border-zinc-200 dark:border-zinc-700", style: { height: 280 }, children: _jsx("iframe", { src: value, title: filenameFromUrl(value), className: "w-full h-full border-0" }) }), _jsxs(Row, { gap: "md", align: "center", children: [_jsx(Div, { "aria-hidden": true, className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300", children: _jsx("span", { className: "text-xs font-bold", children: "PDF" }) }), _jsx("a", { href: value, target: "_blank", rel: "noopener noreferrer", className: "text-sm font-medium text-blue-600 underline break-all dark:text-blue-400", children: filenameFromUrl(value) })] })] })) : (_jsx("a", { href: value, target: "_blank", rel: "noopener noreferrer", className: "text-sm underline break-all text-blue-600 dark:text-blue-400", children: filenameFromUrl(value) })), !disabled && (_jsxs(Row, { wrap: true, gap: "sm", className: "mt-2", children: [isVideo(value) && (enableTrim || enableThumbnail) && (_jsx(Button, { type: "button", onClick: () => onEditVideo(value), variant: "secondary", size: "sm", children: tMediaEditor("editVideo") })), _jsx(Button, { type: "button", onClick: onRemove, variant: "danger", size: "sm", children: tUpload("remove") })] }))] }));
|
|
76
76
|
}
|
|
77
77
|
export function MediaUploadField({ label, value, onChange, onChangeField, onUpload,
|
|
78
78
|
// SB-UNI-Z5 2026-05-13 — `kind` derives accept/maxSizeMB when not set
|
|
79
79
|
// explicitly; explicit props still win. "*" + 50 stay as the floor for
|
|
80
80
|
// back-compat with callers that pass neither.
|
|
81
|
-
kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText, captureSource = "both", captureMode = "photo", enableTrim = true, enableThumbnail = true, onThumbnailChange, showYoutube = true, showExternal = true, onAbort, onStagedUrlsChange, isPersisted = false, }) {
|
|
81
|
+
kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText, captureSource = "both", captureMode = "photo", enableTrim = true, enableThumbnail = true, onThumbnailChange, showYoutube = true, showExternal = true, onAbort, onStagedUrlsChange, isPersisted = false, multiple = false, onAddUrl, }) {
|
|
82
82
|
const [isLoading, setIsLoading] = useState(false);
|
|
83
83
|
const [error, setError] = useState(null);
|
|
84
84
|
const [pendingVideoUrl, setPendingVideoUrl] = useState(null);
|
|
@@ -240,21 +240,35 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
|
|
|
240
240
|
onChange(pendingVideoUrl);
|
|
241
241
|
setPendingVideoUrl(null);
|
|
242
242
|
};
|
|
243
|
-
const
|
|
244
|
-
const file = e.target.files?.[0];
|
|
245
|
-
if (!file)
|
|
246
|
-
return;
|
|
243
|
+
const uploadSingleFile = async (file) => {
|
|
247
244
|
const fileSizeMB = file.size / 1024 / 1024;
|
|
248
245
|
if (fileSizeMB > maxSizeMB) {
|
|
249
|
-
setError(
|
|
250
|
-
return;
|
|
246
|
+
setError(`${file.name}: exceeds ${maxSizeMB}MB limit`);
|
|
247
|
+
return null;
|
|
251
248
|
}
|
|
249
|
+
const url = await onUpload(file);
|
|
250
|
+
stageUrl(url);
|
|
251
|
+
return { url, type: file.type };
|
|
252
|
+
};
|
|
253
|
+
const handleFileChange = async (e) => {
|
|
254
|
+
const files = e.target.files;
|
|
255
|
+
if (!files || files.length === 0)
|
|
256
|
+
return;
|
|
252
257
|
setError(null);
|
|
253
258
|
setIsLoading(true);
|
|
254
259
|
try {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
260
|
+
if (multiple && files.length > 1) {
|
|
261
|
+
for (const file of Array.from(files)) {
|
|
262
|
+
const result = await uploadSingleFile(file);
|
|
263
|
+
if (result)
|
|
264
|
+
onAddUrl?.(result.url);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
const result = await uploadSingleFile(files[0]);
|
|
269
|
+
if (result)
|
|
270
|
+
afterUpload(result.url, result.type);
|
|
271
|
+
}
|
|
258
272
|
}
|
|
259
273
|
catch (err) {
|
|
260
274
|
setError(err instanceof Error ? err.message : "Upload failed");
|
|
@@ -306,5 +320,5 @@ kind, accept: acceptProp, maxSizeMB: maxSizeMBProp, disabled = false, helperText
|
|
|
306
320
|
return (_jsxs(Div, { className: "space-y-2", children: [_jsx(Label, { className: "block text-sm font-medium text-zinc-600 dark:text-zinc-400", 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: [
|
|
307
321
|
"appkit-media-upload__source-tab",
|
|
308
322
|
sourceTab === tab ? "appkit-media-upload__source-tab--active" : "",
|
|
309
|
-
].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, { className: "gap-2", 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, { type: "button", onClick: () => mobileCaptureRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-zinc-200 dark:border-zinc-700 rounded-xl text-sm text-zinc-500 dark:text-zinc-400", children: t("switchToCamera") })), showFileInput && (_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-zinc-200 dark:border-zinc-700 rounded-xl text-sm text-zinc-500 dark:text-zinc-400", children: value ? tUpload("replaceFile") : tUpload("chooseFile") }))] })), isLoading && (_jsxs(Row, { className: "gap-2", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { size: "sm", variant: "secondary", children: tUpload("uploading") })] })), _jsx("input", { ref: fileInputRef, type: "file", accept: accept, 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 }))] }));
|
|
323
|
+
].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, { className: "gap-2", 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, { type: "button", onClick: () => mobileCaptureRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-zinc-200 dark:border-zinc-700 rounded-xl text-sm text-zinc-500 dark:text-zinc-400", children: t("switchToCamera") })), showFileInput && (_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), variant: "ghost", className: "w-full py-3 border-2 border-dashed border-zinc-200 dark:border-zinc-700 rounded-xl text-sm text-zinc-500 dark:text-zinc-400", children: value ? tUpload("replaceFile") : tUpload("chooseFile") }))] })), isLoading && (_jsxs(Row, { className: "gap-2", 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 }))] }));
|
|
310
324
|
}
|
|
@@ -4,14 +4,14 @@ import { formatCurrency } from "../../../utils/number.formatter";
|
|
|
4
4
|
import { getDefaultLocale } from "../../../core/baseline-resolver";
|
|
5
5
|
import { THEME_CONSTANTS } from "../../../tokens";
|
|
6
6
|
const STATUS_COLORS = {
|
|
7
|
-
pending: "bg-
|
|
8
|
-
confirmed: "bg-
|
|
9
|
-
processing: "bg-
|
|
7
|
+
pending: "bg-warning-surface text-warning",
|
|
8
|
+
confirmed: "bg-info-surface text-info",
|
|
9
|
+
processing: "bg-info-surface text-info",
|
|
10
10
|
shipped: "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300",
|
|
11
|
-
delivered: "bg-
|
|
12
|
-
cancelled: "bg-
|
|
13
|
-
refunded: "bg-
|
|
14
|
-
return_requested: "bg-
|
|
11
|
+
delivered: "bg-success-surface text-success",
|
|
12
|
+
cancelled: "bg-error-surface text-error",
|
|
13
|
+
refunded: "bg-warning-surface text-warning",
|
|
14
|
+
return_requested: "bg-warning-surface text-warning",
|
|
15
15
|
returned: "bg-neutral-100 text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300",
|
|
16
16
|
};
|
|
17
17
|
export function OrderCard({ order, onClick, labels = {} }) {
|
|
@@ -12,12 +12,12 @@ function RefundRow({ event, currency }) {
|
|
|
12
12
|
}
|
|
13
13
|
/** Warning triangle icon — inline SVG kept here since it's a single-use decorative element. */
|
|
14
14
|
function WarningIcon() {
|
|
15
|
-
return (_jsx("svg", { className: "h-4 w-4 shrink-0 text-
|
|
15
|
+
return (_jsx("svg", { className: "h-4 w-4 shrink-0 text-warning", fill: "currentColor", viewBox: "0 0 20 20", "aria-hidden": "true", children: _jsx("path", { fillRule: "evenodd", d: "M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495zM10 5a.75.75 0 01.75.75v3.5a.75.75 0 01-1.5 0v-3.5A.75.75 0 0110 5zm0 9a1 1 0 100-2 1 1 0 000 2z", clipRule: "evenodd" }) }));
|
|
16
16
|
}
|
|
17
17
|
export function RefundHistoryTable({ order, className = "" }) {
|
|
18
18
|
const events = order.refunds ?? [];
|
|
19
19
|
const currency = order.currency ?? "INR";
|
|
20
20
|
if (events.length === 0 && order.contestable !== false)
|
|
21
21
|
return null;
|
|
22
|
-
return (_jsxs(Div, { className: `rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900 ${className}`, children: [order.contestable === false && (_jsx(Div, { className: "rounded-t-xl bg-
|
|
22
|
+
return (_jsxs(Div, { className: `rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900 ${className}`, children: [order.contestable === false && (_jsx(Div, { className: "rounded-t-xl bg-warning-surface px-4 py-3", children: _jsxs(Row, { gap: "sm", align: "center", children: [_jsx(WarningIcon, {}), _jsx(Text, { size: "sm", weight: "semibold", className: "text-warning", children: REFUND_COPY.history.nonContestableBanner })] }) })), events.length > 0 && (_jsxs(Div, { className: "p-4", children: [_jsx(Heading, { level: 4, className: "mb-3 text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: REFUND_COPY.history.heading }), _jsx(Stack, { gap: "none", children: events.map((e) => (_jsx(RefundRow, { event: e, currency: currency }, e.refundId))) })] }))] }));
|
|
23
23
|
}
|
|
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* does not talk to the API directly.
|
|
13
13
|
*/
|
|
14
14
|
import { useState } from "react";
|
|
15
|
-
import { Button, Checkbox, Div, Stack, Text, Textarea } from "../../../ui";
|
|
15
|
+
import { Button, Checkbox, Div, Span, Stack, Text, Textarea } from "../../../ui";
|
|
16
16
|
import { REFUND_COPY } from "../../../_internal/shared/features/orders/refund-copy";
|
|
17
17
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
18
18
|
const ACK_COUNT = REFUND_COPY.request.acknowledgments.length;
|
|
@@ -26,7 +26,7 @@ export function RefundRequestView({ order, onSubmitRequest, isLoading = false, c
|
|
|
26
26
|
return (_jsx(Div, { className: `rounded-xl border border-zinc-200 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900 ${className}`, children: _jsx(Text, { size: "sm", color: "muted", children: REFUND_COPY.request.nonRefundableMessage }) }));
|
|
27
27
|
}
|
|
28
28
|
if (order.contestable === false) {
|
|
29
|
-
return (_jsx(Div, { className: `rounded-xl border border-amber-200 bg-
|
|
29
|
+
return (_jsx(Div, { className: `rounded-xl border border-amber-200 bg-warning-surface p-4 dark:border-amber-800 ${className}`, children: _jsx(Text, { size: "sm", className: "text-warning", children: REFUND_COPY.request.alreadyRefundedMessage }) }));
|
|
30
30
|
}
|
|
31
31
|
const handleToggle = (i) => setAcknowledged((prev) => prev.map((v, idx) => (idx === i ? !v : v)));
|
|
32
32
|
const handleSubmit = async () => {
|
|
@@ -38,5 +38,5 @@ export function RefundRequestView({ order, onSubmitRequest, isLoading = false, c
|
|
|
38
38
|
setError(err instanceof Error ? err.message : REFUND_COPY.request.errorFallback);
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
|
-
return (_jsxs(Stack, { gap: "md", className: className, children: [_jsxs(Text, { size: "sm", color: "muted", children: [REFUND_COPY.request.orderTotalLabel, " ", _jsx("
|
|
41
|
+
return (_jsxs(Stack, { gap: "md", className: className, children: [_jsxs(Text, { size: "sm", color: "muted", children: [REFUND_COPY.request.orderTotalLabel, " ", _jsx(Span, { weight: "bold", children: formatCurrency(order.totalPrice / 100, order.currency ?? "INR") })] }), _jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", weight: "semibold", children: [REFUND_COPY.request.reasonLabel, " ", _jsx("span", { "aria-hidden": true, children: "*" })] }), _jsx(Textarea, { value: reason, onChange: (e) => setReason(e.target.value), placeholder: REFUND_COPY.request.reasonPlaceholder, rows: 3, "aria-required": "true", "aria-label": REFUND_COPY.request.reasonLabel })] }), _jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "semibold", children: REFUND_COPY.request.acknowledgeHeading }), REFUND_COPY.request.acknowledgments.map((text, i) => (_jsx(Checkbox, { checked: acknowledged[i], onChange: () => handleToggle(i), label: _jsx(Text, { size: "sm", className: "leading-snug", children: text }), "aria-label": `Acknowledgment ${i + 1}` }, i)))] }), error && (_jsx(Text, { size: "sm", color: "danger", role: "alert", children: error })), _jsx(Button, { variant: "danger", onClick: handleSubmit, disabled: !canSubmit, "aria-busy": isLoading, children: isLoading ? REFUND_COPY.request.submittingLabel : REFUND_COPY.request.submitLabel })] }));
|
|
42
42
|
}
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* Server-only entry point for order repositories.
|
|
5
5
|
*/
|
|
6
6
|
export * from "./actions";
|
|
7
|
-
export { OrderRepository,
|
|
7
|
+
export { OrderRepository, orderRepository, } from "./repository/orders.repository";
|
|
8
8
|
export { GET as getOrderTrackingHandler } from "./api/track/[trackingId]/route";
|
|
@@ -4,6 +4,6 @@
|
|
|
4
4
|
* Server-only entry point for order repositories.
|
|
5
5
|
*/
|
|
6
6
|
export * from "./actions";
|
|
7
|
-
export { OrderRepository,
|
|
7
|
+
export { OrderRepository, orderRepository, } from "./repository/orders.repository";
|
|
8
8
|
// API route handler — re-export for consumer app router stubs
|
|
9
9
|
export { GET as getOrderTrackingHandler } from "./api/track/[trackingId]/route";
|
|
@@ -27,7 +27,7 @@ export function PreOrderActionsClient({ productId, price, currency, depositAmoun
|
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
-
return (_jsxs(Div, { className: "space-y-4", children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5 text-xs text-zinc-500 dark:text-zinc-400", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }))] })), error && (_jsx(Text, { className: "text-xs text-
|
|
30
|
+
return (_jsxs(Div, { className: "space-y-4", children: [price !== null && (_jsxs(Div, { children: [_jsx(Text, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5 text-xs text-zinc-500 dark:text-zinc-400", children: ["Reserve with ", formatCurrency(depositAmount, currency), depositPercent !== null ? ` (${depositPercent}% deposit)` : ""] }))] })), error && (_jsx(Text, { className: "text-xs text-error", children: error })), success && (_jsx(Text, { className: "text-xs text-success", children: "\u2713 Reserved! Redirecting to checkout\u2026" })), _jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to reserve this pre-order. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: "md", className: "w-full", disabled: isPending, onClick: handleReserve, children: isPending ? "Processing…" : "Reserve Now" }), isCancellable && (_jsx(Text, { className: "text-center text-xs text-zinc-500 dark:text-zinc-400", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx("div", { className: "flex flex-wrap gap-1.5", children: tags.map((tag) => (_jsx("span", { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) })), _jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx("div", { className: "flex flex-wrap gap-4 justify-center text-center", children: [
|
|
31
31
|
{ icon: "🔒", label: "Secure\nPayment" },
|
|
32
32
|
{ icon: "📅", label: "Guaranteed\nDelivery" },
|
|
33
33
|
{ icon: "↩", label: "Free\nCancellation" },
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Div, Span } from "../../../ui";
|
|
3
3
|
const STATUS_MAP = {
|
|
4
|
-
pending: { label: "Pending", color: "bg-
|
|
5
|
-
confirmed: { label: "Confirmed", color: "bg-
|
|
6
|
-
ready: { label: "Ready", color: "bg-
|
|
4
|
+
pending: { label: "Pending", color: "bg-warning-surface text-warning" },
|
|
5
|
+
confirmed: { label: "Confirmed", color: "bg-info-surface text-info" },
|
|
6
|
+
ready: { label: "Ready", color: "bg-success-surface text-success" },
|
|
7
7
|
fulfilled: { label: "Fulfilled", color: "bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300" },
|
|
8
|
-
cancelled: { label: "Cancelled", color: "bg-
|
|
8
|
+
cancelled: { label: "Cancelled", color: "bg-error-surface text-error" },
|
|
9
9
|
};
|
|
10
10
|
export function PreOrderBadge({ status, className = "" }) {
|
|
11
11
|
const { label, color } = STATUS_MAP[status] ?? STATUS_MAP.pending;
|
|
@@ -49,7 +49,7 @@ function PreOrderInfoSection({ title, productionStatus, maxPerUser, deliveryDate
|
|
|
49
49
|
} }), (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-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-300 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, { className: "inline-flex items-center rounded-full border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-500 dark:text-zinc-400", children: categoryName })), brand && brandSlug && (_jsx(Link, { href: String(ROUTES.PUBLIC.BRAND_DETAIL(brandSlug)), className: "inline-flex items-center rounded-full border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-600 dark:text-zinc-300 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, { className: "inline-flex items-center rounded-full border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-800/50 px-2.5 py-1 text-xs font-medium text-zinc-500 dark:text-zinc-400", children: brand }))] })), productFeatures && features.length > 0 && (_jsx(FeatureBadgeList, { productFeatureIds: features, features: productFeatures })), !productFeatures && features.length > 0 && (_jsxs(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 px-4 py-3", children: [_jsx(Text, { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-zinc-500 dark:text-zinc-400", children: "About this product" }), _jsx("ul", { className: "space-y-1.5", children: features.map((f, i) => (_jsxs("li", { className: "flex items-start gap-2 text-sm text-zinc-700 dark:text-zinc-300", 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-zinc-600 dark:text-zinc-400 line-clamp-4" })), safeSeller && (_jsx(Div, { className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-3", children: _jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Div, { children: [_jsx(Text, { className: "text-[10px] uppercase tracking-wide text-zinc-400 dark:text-zinc-400 mb-0.5", children: "Sold by" }), _jsx(Text, { className: "text-sm font-semibold text-zinc-800 dark:text-zinc-200", 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" }))] }) }))] }));
|
|
50
50
|
}
|
|
51
51
|
function PreOrderBuyBarPanel({ reserveTarget, reservedCount, progressPct, productId, price, currency, depositAmount, depositPercent, isCancellable, tags, onReserveNow, }) {
|
|
52
|
-
return (_jsxs(Div, { id: "pre-order-buy-bar", className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [reserveTarget > 0 && (_jsxs(Div, { className: "space-y-2", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: [reservedCount, " of ", reserveTarget, " reserved"] }), _jsxs(Span, { className: "text-xs font-semibold text-primary-600 dark:text-primary-400", children: [progressPct, "%"] })] }), _jsx(Div, { className: "h-2 w-full overflow-hidden rounded-full bg-zinc-200 dark:bg-zinc-700", children: _jsx(Div, { className: "h-full rounded-full bg-primary transition-all", style: { width: `${progressPct}%` } }) })] })), 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, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5 text-xs text-zinc-500 dark:text-zinc-400", 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, { className: "text-center text-xs text-zinc-500 dark:text-zinc-400", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) })), _jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "sm",
|
|
52
|
+
return (_jsxs(Div, { id: "pre-order-buy-bar", className: "rounded-xl border border-zinc-100 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/60 p-5 space-y-4", children: [reserveTarget > 0 && (_jsxs(Div, { className: "space-y-2", children: [_jsxs(Row, { justify: "between", align: "center", children: [_jsxs(Text, { className: "text-xs text-zinc-500 dark:text-zinc-400", children: [reservedCount, " of ", reserveTarget, " reserved"] }), _jsxs(Span, { className: "text-xs font-semibold text-primary-600 dark:text-primary-400", children: [progressPct, "%"] })] }), _jsx(Div, { className: "h-2 w-full overflow-hidden rounded-full bg-zinc-200 dark:bg-zinc-700", children: _jsx(Div, { className: "h-full rounded-full bg-primary transition-all", style: { width: `${progressPct}%` } }) })] })), 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, { className: "text-2xl font-bold text-zinc-900 dark:text-zinc-50", children: formatCurrency(price, currency) }), depositAmount !== null && (_jsxs(Text, { className: "mt-0.5 text-xs text-zinc-500 dark:text-zinc-400", 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, { className: "text-center text-xs text-zinc-500 dark:text-zinc-400", children: "\u2713 Free cancellation before production" }))] }), tags.length > 0 && (_jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { className: "rounded-full bg-zinc-100 dark:bg-zinc-800 px-2.5 py-1 text-xs text-zinc-600 dark:text-zinc-300", children: tag }, tag))) }) })), _jsx(Div, { className: "border-t border-zinc-200 dark:border-zinc-700 pt-4", children: _jsx(Row, { wrap: true, gap: "sm", justify: "center", className: "text-center", children: [
|
|
53
53
|
{ icon: "🔒", label: "Secure\nPayment" },
|
|
54
54
|
{ icon: "📅", label: "Guaranteed\nDelivery" },
|
|
55
55
|
{ icon: "↩", label: "Free\nCancellation" },
|
|
@@ -8,8 +8,8 @@ const STATUS_LABELS = {
|
|
|
8
8
|
};
|
|
9
9
|
const STATUS_COLORS = {
|
|
10
10
|
available: "bg-indigo-100 text-indigo-800 dark:bg-indigo-900/40 dark:text-indigo-300",
|
|
11
|
-
shipping_soon: "bg-
|
|
12
|
-
shipped: "bg-
|
|
11
|
+
shipping_soon: "bg-warning-surface text-warning",
|
|
12
|
+
shipped: "bg-success-surface text-success",
|
|
13
13
|
};
|
|
14
14
|
export function PreorderBadge({ shipDate, className }) {
|
|
15
15
|
const status = getPreorderStatus(shipDate);
|
|
@@ -23,5 +23,5 @@ export function CustomSectionsEditor({ sections, onChange, }) {
|
|
|
23
23
|
return;
|
|
24
24
|
onChange([...sections, emptySection()]);
|
|
25
25
|
}
|
|
26
|
-
return (_jsxs(Stack, { gap: "md", children: [sections.map((section, i) => (_jsxs(Div, { className: "rounded-xl border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900/40 p-4 space-y-3", children: [_jsxs(Div, { className: "flex items-center justify-between gap-2", children: [_jsxs(Heading, { level: 4, className: "text-sm font-semibold text-zinc-700 dark:text-zinc-200", children: ["Section ", i + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(i), className: "text-zinc-400 hover:text-red-500 dark:hover:text-red-400 text-xs", children: "Remove" })] }), _jsxs(Div, { children: [_jsxs("label", { htmlFor: `${baseId}-title-${i}`, className: "mb-1 block text-xs font-medium text-zinc-600 dark:text-zinc-400", children: ["Section title ", _jsx("span", { className: "text-
|
|
26
|
+
return (_jsxs(Stack, { gap: "md", children: [sections.map((section, i) => (_jsxs(Div, { className: "rounded-xl border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-900/40 p-4 space-y-3", children: [_jsxs(Div, { className: "flex items-center justify-between gap-2", children: [_jsxs(Heading, { level: 4, className: "text-sm font-semibold text-zinc-700 dark:text-zinc-200", children: ["Section ", i + 1] }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => remove(i), className: "text-zinc-400 hover:text-red-500 dark:hover:text-red-400 text-xs", children: "Remove" })] }), _jsxs(Div, { children: [_jsxs("label", { htmlFor: `${baseId}-title-${i}`, className: "mb-1 block text-xs font-medium text-zinc-600 dark:text-zinc-400", children: ["Section title ", _jsx("span", { className: "text-error", children: "*" })] }), _jsx(Input, { id: `${baseId}-title-${i}`, value: section.title, onChange: (e) => update(i, { title: e.target.value }), placeholder: 'e.g. "Box Contents", "Compatibility", "Grading Details"', maxLength: 80 })] }), _jsxs(Div, { children: [_jsxs("label", { htmlFor: `${baseId}-text-${i}`, className: "mb-1 block text-xs font-medium text-zinc-600 dark:text-zinc-400", children: ["Body text", " ", _jsx("span", { className: "text-zinc-400 font-normal", children: "(optional)" })] }), _jsx("textarea", { id: `${baseId}-text-${i}`, value: section.text ?? "", onChange: (e) => update(i, { text: e.target.value || undefined }), placeholder: "Additional details for this section\u2026", rows: 3, maxLength: 2000, className: "w-full rounded-lg border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-800 dark:text-zinc-100 placeholder:text-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary-500 resize-y" })] }), _jsxs(Div, { children: [_jsxs(Text, { className: "mb-2 text-xs font-medium text-zinc-600 dark:text-zinc-400", children: ["Fields", " ", _jsx("span", { className: "text-zinc-400 font-normal", children: "(optional)" })] }), _jsx(CustomFieldsEditor, { fields: section.fields ?? [], onChange: (fields) => update(i, { fields: fields.length > 0 ? fields : undefined }), showUnit: true })] })] }, section.id))), _jsxs(Div, { className: "flex items-center justify-between", children: [_jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: add, disabled: sections.length >= MAX_CUSTOM_SECTIONS, className: "text-primary-600 dark:text-primary-400", children: "+ Add section" }), _jsxs(Text, { className: "text-xs text-zinc-400 dark:text-zinc-400", children: [sections.length, " / ", MAX_CUSTOM_SECTIONS, " sections"] })] })] }));
|
|
27
27
|
}
|