@mohasinac/appkit 3.5.5 → 3.5.7
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.js +24 -3
- package/dist/_internal/client/features/tour/TourProvider.d.ts +11 -3
- package/dist/_internal/client/features/tour/TourProvider.js +93 -4
- package/dist/_internal/client/scaffolds/index.d.ts +0 -2
- package/dist/_internal/client/scaffolds/index.js +0 -1
- package/dist/_internal/server/features/checkout/actions.js +65 -11
- package/dist/_internal/server/features/orders/adapters.js +3 -0
- package/dist/_internal/shared/actions/action-registry.js +20 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +21 -0
- package/dist/_internal/shared/checkout/order-math.js +24 -0
- package/dist/_internal/shared/features/orders/config.d.ts +1 -1
- package/dist/_internal/shared/features/orders/config.js +1 -1
- package/dist/_internal/shared/fees/calculator.d.ts +13 -0
- package/dist/_internal/shared/fees/calculator.js +8 -0
- package/dist/client-entry.d.ts +2 -2
- package/dist/client-entry.js +2 -2
- package/dist/client.d.ts +8 -0
- package/dist/client.js +8 -0
- package/dist/constants/api-endpoints.d.ts +9 -0
- package/dist/constants/api-endpoints.js +3 -0
- package/dist/features/account/components/UserSidebar.js +1 -1
- package/dist/features/admin/components/AdminBundleEditorView.d.ts +8 -1
- package/dist/features/admin/components/AdminBundleEditorView.js +15 -11
- package/dist/features/admin/components/AdminFeaturesView.js +1 -1
- package/dist/features/admin/components/AdminPayoutsView.js +41 -4
- package/dist/features/admin/components/AdminSidebar.js +1 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +49 -1
- package/dist/features/admin/components/DataTable.js +1 -1
- package/dist/features/admin/components/index.d.ts +0 -2
- package/dist/features/admin/components/index.js +0 -1
- package/dist/features/admin/schemas/firestore.d.ts +8 -0
- package/dist/features/admin/schemas/firestore.js +6 -0
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +1 -1
- package/dist/features/before-after/components/BeforeAfterSlider.js +4 -0
- package/dist/features/blog/components/BlogFeaturedCard.js +1 -1
- package/dist/features/categories/components/BundleDynamicRuleEditor.js +2 -9
- package/dist/features/events/components/EventCard.js +1 -1
- package/dist/features/events/components/EventRafflesSection.d.ts +1 -1
- package/dist/features/events/components/EventRafflesSection.js +3 -3
- package/dist/features/homepage/components/BeforeAfterCard.js +5 -0
- package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +1 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +3 -1
- package/dist/features/homepage/components/GoogleReviewsSection.js +2 -2
- package/dist/features/homepage/components/HeroCarousel.js +1 -1
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +5 -5
- package/dist/features/media/MediaLightbox.js +7 -1
- package/dist/features/media/components/MediaSlider.js +4 -1
- package/dist/features/media/modals/ImageCropModal.js +1 -1
- package/dist/features/media/upload/ImageUpload.js +24 -12
- package/dist/features/messages/hooks/useConversations.d.ts +7 -1
- package/dist/features/messages/hooks/useConversations.js +7 -6
- package/dist/features/orders/actions/order-actions.d.ts +10 -0
- package/dist/features/orders/actions/order-actions.js +57 -4
- package/dist/features/orders/components/MarketplaceOrderCard.js +1 -1
- package/dist/features/orders/schemas/firestore.d.ts +16 -0
- package/dist/features/orders/types/index.d.ts +2 -0
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +1 -1
- package/dist/features/products/components/MarketplacePrizeDrawCard.js +1 -1
- package/dist/features/products/components/PrizeDrawsSection.d.ts +1 -1
- package/dist/features/products/components/PrizeDrawsSection.js +3 -2
- package/dist/features/products/components/ProductForm.js +4 -9
- package/dist/features/products/components/ProductGrid.js +1 -1
- package/dist/features/products/schemas/firestore.d.ts +4 -0
- package/dist/features/promotions/components/CouponCard.js +1 -1
- package/dist/features/scams/actions/scam-actions.d.ts +12 -0
- package/dist/features/scams/actions/scam-actions.js +31 -0
- package/dist/features/scams/components/SellerTrustBadge.d.ts +16 -0
- package/dist/features/scams/components/SellerTrustBadge.js +21 -0
- package/dist/features/scams/components/index.d.ts +2 -0
- package/dist/features/scams/components/index.js +1 -0
- package/dist/features/seller/components/SellerBundlesView.d.ts +5 -5
- package/dist/features/seller/components/SellerBundlesView.js +81 -84
- package/dist/features/seller/components/SellerProductShell.d.ts +2 -0
- package/dist/features/seller/components/SellerProductShell.js +11 -5
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/shipments/components/ShipmentItemLinkModal.js +5 -5
- package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
- package/dist/features/stores/components/StoreDetailLayoutView.d.ts +8 -1
- package/dist/features/stores/components/StoreDetailLayoutView.js +9 -3
- package/dist/features/stores/components/StoreHeader.d.ts +4 -1
- package/dist/features/stores/components/StoreHeader.js +3 -2
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -5
- package/dist/jobs.d.ts +2 -0
- package/dist/jobs.js +6 -0
- package/dist/next/routing/route-map.d.ts +3 -0
- package/dist/next/routing/route-map.js +1 -0
- package/dist/react/hooks/useClickOutside.js +3 -0
- package/dist/security/rate-limit.d.ts +5 -0
- package/dist/security/rate-limit.js +2 -0
- package/dist/seed/categories-seed-data.js +180 -1
- package/dist/seed/grouped-listings-seed-data.d.ts +2 -0
- package/dist/seed/products-preorders-seed-data.d.ts +2 -0
- package/dist/seed/site-settings-seed-data.js +9 -2
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.js +5 -0
- package/dist/styles.css +11 -4
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/cobalt-night.d.ts +17 -0
- package/dist/tokens/themes/cobalt-night.js +50 -0
- package/dist/tokens/themes/index.d.ts +9 -1
- package/dist/tokens/themes/index.js +12 -1
- package/dist/tokens/themes/sunset.d.ts +18 -0
- package/dist/tokens/themes/sunset.js +51 -0
- package/dist/tokens/tokens.css +10 -3
- package/dist/ui/components/BaseListingCard.d.ts +2 -1
- package/dist/ui/components/BaseListingCard.js +4 -4
- package/dist/ui/components/Card.d.ts +1 -1
- package/dist/ui/components/Card.js +2 -2
- package/dist/ui/components/Div.js +2 -2
- package/dist/ui/components/Form.d.ts +1 -1
- package/dist/ui/components/Form.js +2 -2
- package/dist/ui/components/Input.js +1 -0
- package/dist/ui/components/ListingToolbar.js +1 -1
- package/dist/ui/components/QuickCreateModal.style.css +1 -1
- package/dist/ui/components/Slider.js +5 -1
- package/package.json +3 -2
|
@@ -33,6 +33,6 @@ export function EventCard({ event, labels = {}, onParticipate, className = "", s
|
|
|
33
33
|
const msLeft = endsAt.getTime() - now.getTime();
|
|
34
34
|
const daysLeft = Math.max(0, Math.ceil(msLeft / (1000 * 60 * 60 * 24)));
|
|
35
35
|
const detailHref = String(ROUTES.PUBLIC.EVENT_DETAIL(event.slug ?? event.id));
|
|
36
|
-
return (_jsxs(Article, { border: "default", rounded: "xl", shadow: "hover-md", className: `group relative h-full ${LAYOUT.cardHeight.event} overflow-hidden bg-[var(--appkit-color-surface)] ${isSelected ? "border-primary outline outline-2 outline-primary" : " "} ${className}`, onMouseDown: onSelect && !isSelected ? longPress.onMouseDown : undefined, onMouseUp: onSelect && !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: onSelect && !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: onSelect && !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: onSelect && !isSelected ? longPress.onTouchEnd : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(event.id, !isSelected); }, label: isSelected ? "Deselect event" : "Select event", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100 transition-opacity" })), _jsx(Link, { href: detailHref, className: "block flex-shrink-0", children: event.coverImageUrl ? (_jsx(Div, { className: `relative aspect-video ${__O.hidden}`, children: _jsx(MediaImage, { src: event.coverImageUrl, alt: safeTitle, size: "card", className: "transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Row, { surface: "muted", className: "aspect-video", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-40", size: "5xl", "aria-hidden": "true", children: TYPE_ICONS[event.type] }) })) }), _jsxs(Stack, { className: `flex-1 ${__P.p4}`, children: [_jsxs(Row, { className: "mb-2", align: "start", justify: "between", gap: "sm", children: [_jsx(Span, { size: "lg", "aria-hidden": "true", children: TYPE_ICONS[event.type] }), _jsx(EventStatusBadge, { status: event.status })] }), _jsx(Link, { href: detailHref, className: "block", children: _jsx(Heading, { level: 3, className: "text-[var(--appkit-color-text)] leading-snug mb-1 group-hover:text-primary transition-colors", size: "base", weight: "semibold", children: safeTitle }) }), _jsx(RichText, { html: normalizeRichTextHtml(event.description ?? ""), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mb-3 line-clamp-3 text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)]" }), _jsxs(Row, { color: "muted", textSize: "xs", className: "mb-3 mt-auto", align: "center", justify: "between", children: [event.status === EVENT_FIELDS.STATUS_VALUES.ACTIVE &&
|
|
36
|
+
return (_jsxs(Article, { border: "default", rounded: "xl", shadow: "hover-md", className: `group relative h-full ${LAYOUT.cardHeight.event} overflow-hidden bg-[var(--appkit-color-surface)] ${isSelected ? "border-primary outline outline-2 outline-primary" : " "} ${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, onTouchCancel: onSelect && !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => { e.preventDefault(); onSelect(event.id, !isSelected); }, label: isSelected ? "Deselect event" : "Select event", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100 transition-opacity" })), _jsx(Link, { href: detailHref, className: "block flex-shrink-0", children: event.coverImageUrl ? (_jsx(Div, { className: `relative aspect-video ${__O.hidden}`, children: _jsx(MediaImage, { src: event.coverImageUrl, alt: safeTitle, size: "card", className: "transition-transform duration-300 group-hover:scale-105" }) })) : (_jsx(Row, { surface: "muted", className: "aspect-video", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-40", size: "5xl", "aria-hidden": "true", children: TYPE_ICONS[event.type] }) })) }), _jsxs(Stack, { className: `flex-1 ${__P.p4}`, children: [_jsxs(Row, { className: "mb-2", align: "start", justify: "between", gap: "sm", children: [_jsx(Span, { size: "lg", "aria-hidden": "true", children: TYPE_ICONS[event.type] }), _jsx(EventStatusBadge, { status: event.status })] }), _jsx(Link, { href: detailHref, className: "block", children: _jsx(Heading, { level: 3, className: "text-[var(--appkit-color-text)] leading-snug mb-1 group-hover:text-primary transition-colors", size: "base", weight: "semibold", children: safeTitle }) }), _jsx(RichText, { html: normalizeRichTextHtml(event.description ?? ""), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mb-3 line-clamp-3 text-[length:var(--appkit-text-sm)] text-[var(--appkit-color-text-muted)]" }), _jsxs(Row, { color: "muted", textSize: "xs", className: "mb-3 mt-auto", align: "center", justify: "between", children: [event.status === EVENT_FIELDS.STATUS_VALUES.ACTIVE &&
|
|
37
37
|
daysLeft > 0 && _jsxs(Span, { children: ["\u23F1 ", daysLeft, "d remaining"] }), _jsxs(Span, { children: ["\uD83D\uDC65 ", event.stats.totalEntries, " ", labels.entries ?? "entries"] })] }), event.status === EVENT_FIELDS.STATUS_VALUES.ACTIVE && onParticipate ? (_jsx(Button, { rounded: "lg", type: "button", onClick: () => onParticipate(event), className: "w-full bg-primary py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-medium text-white transition-colors hover:bg-primary-600", children: labels.participate ?? "Participate" })) : (_jsxs(TextLink, { rounded: "lg", paddingX: "sm", paddingY: "xs", href: detailHref, layout: "inline-flex", align: "center", justify: "center", gap: "xs", className: "w-full border border-[var(--appkit-color-border)] transition-colors hover:bg-[var(--appkit-color-bg)]", color: "primary", size: "sm", weight: "medium", children: [labels.viewDetails ?? "View details", " \u2192"] }))] })] }));
|
|
38
38
|
}
|
|
@@ -6,4 +6,4 @@ export interface EventRafflesSectionProps {
|
|
|
6
6
|
* Renders a live-raffle/spin-wheel strip on the homepage.
|
|
7
7
|
* W1-38 (2026-05-23): fetches events with hasRaffle=true and renders the EventCard grid.
|
|
8
8
|
*/
|
|
9
|
-
export declare function EventRafflesSection({ config, }: EventRafflesSectionProps): Promise<import("react").JSX.Element>;
|
|
9
|
+
export declare function EventRafflesSection({ config, }: EventRafflesSectionProps): Promise<import("react").JSX.Element | null>;
|
|
@@ -2,9 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { normalizeError } from "../../../errors/normalize";
|
|
3
3
|
import { sieveFilter, sieveAnd, SIEVE_OP } from "@mohasinac/appkit";
|
|
4
4
|
import { sortBy } from "@mohasinac/appkit";
|
|
5
|
-
import Link from "next/link";
|
|
6
5
|
import { Container, Div, Heading, Section, Stack, Text, } from "../../../ui";
|
|
7
|
-
import { ROUTES } from "../../../next";
|
|
8
6
|
import { eventRepository } from "../repository/events.repository";
|
|
9
7
|
import { EventCard } from "./EventCard";
|
|
10
8
|
/**
|
|
@@ -28,5 +26,7 @@ export async function EventRafflesSection({ config, }) {
|
|
|
28
26
|
void normalizeError(_err);
|
|
29
27
|
events = [];
|
|
30
28
|
}
|
|
31
|
-
|
|
29
|
+
if (events.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
return (_jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), subtitle ? (_jsx(Text, { size: "sm", color: "muted", children: subtitle })) : null] }), _jsx(Div, { gap: "3", className: "fluid-grid-card", children: events.map((event) => (_jsx(EventCard, { event: event }, event.id))) })] }) }) }));
|
|
32
32
|
}
|
|
@@ -45,11 +45,16 @@ export function BeforeAfterCard({ item, beforeLabel = "Before", afterLabel = "Af
|
|
|
45
45
|
window.addEventListener("mouseup", onUp);
|
|
46
46
|
window.addEventListener("touchmove", onMove, { passive: false });
|
|
47
47
|
window.addEventListener("touchend", onUp);
|
|
48
|
+
// Without this, an interrupted touch (OS/browser gesture takeover)
|
|
49
|
+
// never fires touchend — `dragging` stays true forever, leaking these
|
|
50
|
+
// window-level listeners for the rest of the component's lifetime.
|
|
51
|
+
window.addEventListener("touchcancel", onUp);
|
|
48
52
|
return () => {
|
|
49
53
|
window.removeEventListener("mousemove", onMove);
|
|
50
54
|
window.removeEventListener("mouseup", onUp);
|
|
51
55
|
window.removeEventListener("touchmove", onMove);
|
|
52
56
|
window.removeEventListener("touchend", onUp);
|
|
57
|
+
window.removeEventListener("touchcancel", onUp);
|
|
53
58
|
};
|
|
54
59
|
}, [dragging, updatePosition]);
|
|
55
60
|
return (_jsxs(Div, { className: `border-border ${__O.hidden} border bg-card`, rounded: "2xl", shadow: "sm", children: [_jsxs(Div, { ref: containerRef, className: "relative aspect-[4/3] cursor-col-resize overflow-hidden select-none", onMouseDown: (e) => {
|
|
@@ -17,4 +17,4 @@ export interface FeaturedBundlesSectionProps {
|
|
|
17
17
|
bundleSlug: string;
|
|
18
18
|
}) => Promise<unknown>;
|
|
19
19
|
}
|
|
20
|
-
export declare function FeaturedBundlesSection({ title, description, initialItems, className, onBuyNow, }: FeaturedBundlesSectionProps): React.JSX.Element;
|
|
20
|
+
export declare function FeaturedBundlesSection({ title, description, initialItems, className, onBuyNow, }: FeaturedBundlesSectionProps): React.JSX.Element | null;
|
|
@@ -15,7 +15,9 @@ const __O = {
|
|
|
15
15
|
const PLACEHOLDER_EMOJI = "📦";
|
|
16
16
|
export function FeaturedBundlesSection({ title = BUNDLE_COPY.featuredDefaultTitle, description, initialItems = [], className = "", onBuyNow, }) {
|
|
17
17
|
const items = initialItems.filter((c) => c.categoryType === "bundle");
|
|
18
|
-
|
|
18
|
+
if (items.length === 0)
|
|
19
|
+
return null;
|
|
20
|
+
return (_jsx(Section, { className: `${className}`, padding: "y-2xl", children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", wrap: true, children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "2xl", weight: "semibold", color: "primary", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES), className: "text-[length:var(--appkit-text-sm)] font-medium text-[var(--appkit-color-primary)] hover:underline", children: BUNDLE_COPY.featured.viewAll })] }), _jsx(Div, { layout: "grid", gap: "3", className: "grid-cols-2 sm:grid-cols-3 lg:grid-cols-4", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle, onBuyNow: onBuyNow }, bundle.id))) })] }) }));
|
|
19
21
|
}
|
|
20
22
|
function FeaturedBundleCard({ bundle, onBuyNow }) {
|
|
21
23
|
const stock = bundle.bundleStockStatus ?? "in_stock";
|
|
@@ -31,10 +31,10 @@ function ReviewCard({ review, showRating, showDate, }) {
|
|
|
31
31
|
}
|
|
32
32
|
// --- Empty / Not Configured States -------------------------------------------
|
|
33
33
|
function NotConfiguredState() {
|
|
34
|
-
return (_jsxs(Stack, { textSize: "sm", justify: "center", color: "faint", padding: "y-4xl", align: "
|
|
34
|
+
return (_jsxs(Stack, { textSize: "sm", justify: "center", color: "faint", padding: "y-4xl", align: "center", children: [_jsxs("svg", { className: "w-10 h-10 mb-3", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: [_jsx("circle", { cx: "12", cy: "12", r: "10" }), _jsx("path", { d: "M12 8v4m0 4h.01" })] }), "Google Reviews not configured. Add your Google Maps API key in Site Settings."] }));
|
|
35
35
|
}
|
|
36
36
|
function EmptyState() {
|
|
37
|
-
return (_jsxs(Stack, { textSize: "sm", justify: "center", color: "faint", padding: "y-4xl", align: "
|
|
37
|
+
return (_jsxs(Stack, { textSize: "sm", justify: "center", color: "faint", padding: "y-4xl", align: "center", children: [_jsx("svg", { className: "w-10 h-10 mb-3", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "1.5", children: _jsx("path", { d: "M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" }) }), "No reviews yet."] }));
|
|
38
38
|
}
|
|
39
39
|
export async function GoogleReviewsSection(config) {
|
|
40
40
|
const { placeId: configPlaceId, maxReviews = 6, minRating = 0, layout = "grid", showRating = true, showDate = true, linkToGoogleMaps = true, googleMapsUrl, } = config;
|
|
@@ -177,7 +177,7 @@ export function HeroCarousel({ initialSlides, push } = {}) {
|
|
|
177
177
|
}
|
|
178
178
|
};
|
|
179
179
|
if (isLoading) {
|
|
180
|
-
return (_jsx(Div, { className: `relative w-full ${heightClass} ${HERO_PLACEHOLDER_BG} animate-pulse`, children: _jsx(Div, { layout: "flex", align: "center", justify: "
|
|
180
|
+
return (_jsx(Div, { className: `relative w-full ${heightClass} ${HERO_PLACEHOLDER_BG} animate-pulse`, children: _jsx(Div, { layout: "flex", align: "center", justify: "center", className: `${POSITION_FILL} p-[var(--appkit-space-4)]`, children: _jsx(Text, { variant: "secondary", children: "Loading..." }) }) }));
|
|
181
181
|
}
|
|
182
182
|
if (!slides || slides.length === 0) {
|
|
183
183
|
return (_jsxs(Div, { gap: "4", layout: "flex-col", align: "start", justify: "center", className: "relative w-full min-h-[260px] md:min-h-[60vh] bg-[image:var(--appkit-gradient-section-cool)] px-[var(--appkit-space-6)]", children: [_jsx(Heading, { level: 2, className: "text-primary", size: "2xl", weight: "bold", align: "start", children: "Coming Soon" }), _jsx(Text, { variant: "secondary", className: "max-w-md", align: "start", children: "Amazing deals are on their way. Stay tuned!" })] }));
|
|
@@ -51,5 +51,5 @@ export function NavbarLayout({ items, activeHref, id = "main-navbar", ariaLabel
|
|
|
51
51
|
if (inline) {
|
|
52
52
|
return (_jsx(Ul, { "aria-label": ariaLabel, className: "hidden xl:flex items-center justify-end gap-[var(--appkit-space-0-5)] xl:gap-[var(--appkit-space-1)] w-full", children: items.map((item) => (_jsx(Li, { children: renderItem ? (renderItem(item, activeHref === item.href)) : (_jsx(DefaultNavItem, { item: item, isActive: activeHref === item.href })) }, item.href))) }));
|
|
53
53
|
}
|
|
54
|
-
return (_jsx(Nav, { border: "subtle", id: id, "aria-label": ariaLabel, className: "hidden lg:block bg-
|
|
54
|
+
return (_jsx(Nav, { border: "subtle", id: id, "aria-label": ariaLabel, className: "hidden lg:block bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b", children: _jsxs(Row, { className: "container mx-auto sm:px-[var(--appkit-space-6)] lg:px-[var(--appkit-space-8)] max-w-[1920px] h-10", padding: "x-md", align: "center", children: [_jsxs(Div, { className: `relative flex-1 ${__O.hidden}`, children: [canScrollLeft && (_jsx(NavbarChevron, { direction: "left", onClick: () => scroll("left") })), _jsx(Ul, { ref: scrollRef, className: "flex items-center justify-end gap-[var(--appkit-space-0-5)] lg:gap-[var(--appkit-space-1)] overflow-x-auto [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]", children: items.map((item) => (_jsx(Li, { className: "shrink-0", children: renderItem ? (renderItem(item, activeHref === item.href)) : (_jsx(DefaultNavItem, { item: item, isActive: activeHref === item.href })) }, item.href))) }), canScrollRight && (_jsx(NavbarChevron, { direction: "right", onClick: () => scroll("right") }))] }), rightSlot && _jsx(Div, { className: "shrink-0 ml-2", children: rightSlot })] }) }));
|
|
55
55
|
}
|
|
@@ -22,18 +22,18 @@ export function TitleBarLayout({ onToggleSidebar, sidebarOpen, onSearchToggle, s
|
|
|
22
22
|
const hamburgerBtn = !hideSidebarToggle ? (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": sidebarOpen ? "Close menu" : hasDashboardNav ? "Open dashboard navigation" : "Open menu", "aria-expanded": sidebarOpen, "aria-controls": "secondary-sidebar", onClick: hasDashboardNav && onToggleDashboardNav ? onToggleDashboardNav : onToggleSidebar, className: iconBtn, children: _jsx("svg", { className: "w-5 h-5", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: sidebarOpen ? (_jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" })) : (_jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 6h16M4 12h16M4 18h16" })) }) })) : null;
|
|
23
23
|
// Compare is always lg+ (desktop-only feature, less critical on mobile)
|
|
24
24
|
const compareEl = compareHref ? (_jsx(Link, { href: compareHref, "aria-label": "Compare items", className: `${iconBtn} hidden lg:flex`, 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: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" }) }) })) : null;
|
|
25
|
-
const searchBtn = (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Search", onClick: onSearchToggle, className: 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: "M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" }) }) }));
|
|
25
|
+
const searchBtn = (_jsx(Button, { type: "button", variant: "ghost", size: "sm", "aria-label": "Search", onClick: onSearchToggle, className: iconBtn, "data-tour": "nav-search", 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: "M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z" }) }) }));
|
|
26
26
|
// Dedicated notification bell — rendered immediately before the wishlist
|
|
27
27
|
// icon (per layout request). Falls back to invisible when no href is set;
|
|
28
28
|
// the count badge mirrors the wishlist/cart pattern.
|
|
29
29
|
const notificationsEl = notificationsHref ? (_jsxs(Link, { href: notificationsHref, "aria-label": `Notifications${unreadNotificationCount > 0 ? `, ${unreadNotificationCount} unread` : ""}`, 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: "M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" }) }), unreadNotificationCount > 0 && (_jsx(Span, { className: countBadge, children: unreadNotificationCount > 99 ? "99+" : unreadNotificationCount }))] })) : null;
|
|
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
|
-
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
|
-
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;
|
|
30
|
+
const wishlistEl = wishlistHref ? (_jsxs(Link, { href: wishlistHref, "aria-label": `Wishlist${wishlistCount > 0 ? `, ${wishlistCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-wishlist", 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
|
+
const cartEl = cartHref ? (_jsxs(Link, { href: cartHref, "aria-label": `Cart${cartCount > 0 ? `, ${cartCount} items` : ""}`, className: `relative ${iconBtn}`, "data-tour": "nav-cart", 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
|
+
const profileEl = profileHref ? (_jsx(Link, { href: profileHref, "aria-label": user ? `Profile — ${user.displayName ?? user.email}` : "Sign in", className: `relative ${iconBtn}`, "data-tour": "nav-profile", children: _jsx(Avatar, { src: user?.photoURL ?? undefined, name: user?.displayName ?? user?.email ?? undefined, alt: user?.displayName ?? "Profile", size: "sm" }) })) : null;
|
|
33
33
|
const authButtonsEl = !user && (loginHref || registerHref) ? (_jsxs(Row, { gap: "xs", className: "hidden lg:flex", children: [loginHref && (_jsx(Link, { href: loginHref, className: "px-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-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-[var(--appkit-space-3)] py-[var(--appkit-space-1-5)] text-[length:var(--appkit-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-
|
|
36
|
+
return (_jsxs(BlockHeader, { id: id, className: `sticky top-0 z-50 bg-[var(--appkit-color-bg)]/95 backdrop-blur-md border-b border-[var(--appkit-color-border-subtle)] shadow-sm ${className}`, children: [promoStripText && (_jsx(Section, { color: "inverse", tone: "accent-banner", className: "text-[length:var(--appkit-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 ml-auto", 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-[var(--appkit-space-1)]", children: [notificationsEl, wishlistEl, cartEl, profileEl] }))] })] }));
|
|
39
39
|
}
|
|
@@ -220,10 +220,16 @@ export function MediaLightbox({ items, initialIndex = 0, isOpen, onClose, labels
|
|
|
220
220
|
}
|
|
221
221
|
touchState.current = {};
|
|
222
222
|
}, [zoom, goNext, goPrev]);
|
|
223
|
+
// An interrupted gesture (OS/browser takeover) never fires touchend —
|
|
224
|
+
// without this, stale touchState.current values corrupt the math for the
|
|
225
|
+
// next unrelated pinch/swipe gesture.
|
|
226
|
+
const handleTouchCancel = useCallback(() => {
|
|
227
|
+
touchState.current = {};
|
|
228
|
+
}, []);
|
|
223
229
|
if (!isOpen || items.length === 0)
|
|
224
230
|
return null;
|
|
225
231
|
const current = items[index];
|
|
226
|
-
return (_jsxs(Div, { layout: "flex", ref: containerRef, className: "fixed inset-0 z-[var(--appkit-z-tooltip)] bg-black/95 flex-col", role: "dialog", "aria-modal": "true", "aria-label": L.lightboxTitle, children: [_jsx(Div, { ref: imageAreaRef, className: "flex-1 relative overflow-hidden select-none", onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, style: {
|
|
232
|
+
return (_jsxs(Div, { layout: "flex", ref: containerRef, className: "fixed inset-0 z-[var(--appkit-z-tooltip)] bg-black/95 flex-col", role: "dialog", "aria-modal": "true", "aria-label": L.lightboxTitle, children: [_jsx(Div, { ref: imageAreaRef, className: "flex-1 relative overflow-hidden select-none", onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onTouchCancel: handleTouchCancel, style: {
|
|
227
233
|
cursor: zoom > 1 ? (isDragging ? "grabbing" : "grab") : "default",
|
|
228
234
|
touchAction: "none",
|
|
229
235
|
}, onClick: (e) => {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
export function MediaSlider({ value, min = 0, max = 100, step = 1, onChange, onChangeEnd, className = "", }) {
|
|
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)),
|
|
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)),
|
|
4
|
+
// audit-touch-handler-ok: native <input type="range"> owns its own
|
|
5
|
+
// drag/cancel state; nothing here persists across a cancelled touch.
|
|
6
|
+
onTouchEnd: (e) => onChangeEnd?.(parseFloat(e.target.value)), className: [
|
|
4
7
|
"w-full h-2 appearance-none cursor-pointer rounded-full",
|
|
5
8
|
"bg-zinc-200 bg-[var(--appkit-color-border)]",
|
|
6
9
|
"accent-primary-600 dark:accent-secondary-400",
|
|
@@ -89,7 +89,7 @@ export function ImageCropModal({ isOpen, imageUrl, onClose, onSave, initialCropD
|
|
|
89
89
|
setZoom(1);
|
|
90
90
|
setPosition({ x: 50, y: 50 });
|
|
91
91
|
};
|
|
92
|
-
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, title: t("cropTitle"), size: "lg", children: _jsxs(Stack, { gap: "3", children: [_jsx(Text, { variant: "secondary", size: "xs", children: t("cropInstruction") }), _jsxs(Div, { ref: containerRef, className: "relative w-full max-w-sm mx-auto aspect-square overflow-hidden cursor-move max-h-[280px] touch-none", surface: "subtle", rounded: "lg", "data-disable-swipe": "true", tabIndex: 0, role: "application", "aria-label": t("cropInstruction"), onKeyDown: handleKeyDown, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, children: [_jsx("img", { src: imageUrl, alt: t("cropPreviewAlt"), draggable: false, className: "absolute select-none pointer-events-none object-cover", style: {
|
|
92
|
+
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, title: t("cropTitle"), size: "lg", children: _jsxs(Stack, { gap: "3", children: [_jsx(Text, { variant: "secondary", size: "xs", children: t("cropInstruction") }), _jsxs(Div, { ref: containerRef, className: "relative w-full max-w-sm mx-auto aspect-square overflow-hidden cursor-move max-h-[280px] touch-none", surface: "subtle", rounded: "lg", "data-disable-swipe": "true", tabIndex: 0, role: "application", "aria-label": t("cropInstruction"), onKeyDown: handleKeyDown, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseUp, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onTouchCancel: handleTouchEnd, children: [_jsx("img", { src: imageUrl, alt: t("cropPreviewAlt"), draggable: false, className: "absolute select-none pointer-events-none object-cover", style: {
|
|
93
93
|
width: `${zoom * 100}%`,
|
|
94
94
|
height: `${zoom * 100}%`,
|
|
95
95
|
left: `${position.x}%`,
|
|
@@ -11,7 +11,7 @@ import { normalizeError } from "../../../errors/normalize";
|
|
|
11
11
|
import { useState, useRef } from "react";
|
|
12
12
|
import { useTranslations } from "next-intl";
|
|
13
13
|
import { useCamera } from "../../../react";
|
|
14
|
-
import { Alert, Button, Div, Label, Progress, Row, Spinner, Stack, Text } from "../../../ui";
|
|
14
|
+
import { Alert, Button, Div, Label, Progress, Row, Spinner, Stack, Text, useToast } from "../../../ui";
|
|
15
15
|
import { MediaImage } from "../MediaImage";
|
|
16
16
|
import { ImageCropModal } from "../modals/ImageCropModal";
|
|
17
17
|
import { ImageEditor } from "../modals/ImageEditor";
|
|
@@ -51,6 +51,7 @@ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/
|
|
|
51
51
|
const tUpload = useTranslations("upload");
|
|
52
52
|
const tMediaEditor = useTranslations("mediaEditor");
|
|
53
53
|
const { isSupported: isCameraSupported } = useCamera();
|
|
54
|
+
const { showToast } = useToast();
|
|
54
55
|
const showCamera = captureSource === "camera-only" ||
|
|
55
56
|
(captureSource === "both" && captureMode === "camera");
|
|
56
57
|
const showFileInput = captureSource === "file-only" ||
|
|
@@ -65,12 +66,18 @@ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/
|
|
|
65
66
|
onChange?.(url);
|
|
66
67
|
if (cropData)
|
|
67
68
|
onCropDataChange?.(cropData);
|
|
69
|
+
return true;
|
|
68
70
|
}
|
|
69
71
|
catch (err) {
|
|
70
72
|
void normalizeError(err);
|
|
71
73
|
const message = err instanceof Error ? err.message : "Upload failed";
|
|
72
74
|
setError(message);
|
|
73
75
|
setPreview(currentImage || "");
|
|
76
|
+
// The crop/advanced-editor modal stays open on failure (callers only
|
|
77
|
+
// close it when this resolves true) — without a toast too, the error
|
|
78
|
+
// was easy to miss below the button the closed modal used to cover.
|
|
79
|
+
showToast(message, "error");
|
|
80
|
+
return false;
|
|
74
81
|
}
|
|
75
82
|
finally {
|
|
76
83
|
setUploading(false);
|
|
@@ -97,12 +104,15 @@ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/
|
|
|
97
104
|
};
|
|
98
105
|
reader.readAsDataURL(file);
|
|
99
106
|
};
|
|
100
|
-
const handleCropSave = (cropData) => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
107
|
+
const handleCropSave = async (cropData) => {
|
|
108
|
+
if (!pendingFile)
|
|
109
|
+
return;
|
|
110
|
+
const ok = await performUpload(pendingFile, cropData);
|
|
111
|
+
if (ok) {
|
|
112
|
+
setShowCropModal(false);
|
|
113
|
+
setCropPreviewUrl(null);
|
|
114
|
+
setPendingFile(null);
|
|
115
|
+
}
|
|
106
116
|
};
|
|
107
117
|
const handleCropClose = () => {
|
|
108
118
|
setShowCropModal(false);
|
|
@@ -112,11 +122,13 @@ export function ImageUpload({ currentImage, onUpload, onChange, accept = "image/
|
|
|
112
122
|
setPendingFile(null);
|
|
113
123
|
}
|
|
114
124
|
};
|
|
115
|
-
const handleAdvancedEditorSave = (croppedFile) => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
const handleAdvancedEditorSave = async (croppedFile) => {
|
|
126
|
+
const ok = await performUpload(croppedFile);
|
|
127
|
+
if (ok) {
|
|
128
|
+
setShowAdvancedEditor(false);
|
|
129
|
+
setCropPreviewUrl(null);
|
|
130
|
+
setPendingFile(null);
|
|
131
|
+
}
|
|
120
132
|
};
|
|
121
133
|
const openEditor = (url) => {
|
|
122
134
|
setCropPreviewUrl(url);
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { ConversationDocument } from "../schemas/firestore";
|
|
2
|
+
export interface UseConversationsOptions {
|
|
3
|
+
/** Override the list endpoint — e.g. the seller-scoped /api/store/conversations. Defaults to the buyer endpoint. */
|
|
4
|
+
endpoint?: string;
|
|
5
|
+
/** Which per-conversation unread counter to sum into totalUnread. Defaults to "unreadBuyer". */
|
|
6
|
+
unreadField?: "unreadBuyer" | "unreadSeller";
|
|
7
|
+
}
|
|
2
8
|
export interface UseConversationsReturn {
|
|
3
9
|
conversations: ConversationDocument[];
|
|
4
10
|
isLoading: boolean;
|
|
@@ -6,4 +12,4 @@ export interface UseConversationsReturn {
|
|
|
6
12
|
totalUnread: number;
|
|
7
13
|
refetch: () => Promise<void>;
|
|
8
14
|
}
|
|
9
|
-
export declare function useConversations(userId: string | null | undefined): UseConversationsReturn;
|
|
15
|
+
export declare function useConversations(userId: string | null | undefined, opts?: UseConversationsOptions): UseConversationsReturn;
|
|
@@ -11,11 +11,12 @@ import { normalizeError } from "../../../errors/normalize";
|
|
|
11
11
|
import { apiClient } from "../../../http/ApiClient";
|
|
12
12
|
import { CONVERSATION_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
13
13
|
const LIST_ENDPOINT = CONVERSATION_ENDPOINTS.LIST;
|
|
14
|
-
async function fetchList() {
|
|
15
|
-
const data = await apiClient.get(
|
|
14
|
+
async function fetchList(endpoint) {
|
|
15
|
+
const data = await apiClient.get(endpoint);
|
|
16
16
|
return data.items ?? [];
|
|
17
17
|
}
|
|
18
|
-
export function useConversations(userId) {
|
|
18
|
+
export function useConversations(userId, opts = {}) {
|
|
19
|
+
const { endpoint = LIST_ENDPOINT, unreadField = "unreadBuyer" } = opts;
|
|
19
20
|
const [conversations, setConversations] = useState([]);
|
|
20
21
|
const [isLoading, setIsLoading] = useState(false);
|
|
21
22
|
const [error, setError] = useState(null);
|
|
@@ -27,7 +28,7 @@ export function useConversations(userId) {
|
|
|
27
28
|
setIsLoading(true);
|
|
28
29
|
setError(null);
|
|
29
30
|
try {
|
|
30
|
-
setConversations(await fetchList());
|
|
31
|
+
setConversations(await fetchList(endpoint));
|
|
31
32
|
}
|
|
32
33
|
catch (e) {
|
|
33
34
|
void normalizeError(e);
|
|
@@ -36,7 +37,7 @@ export function useConversations(userId) {
|
|
|
36
37
|
finally {
|
|
37
38
|
setIsLoading(false);
|
|
38
39
|
}
|
|
39
|
-
}, [userId]);
|
|
40
|
+
}, [userId, endpoint]);
|
|
40
41
|
useEffect(() => {
|
|
41
42
|
void refetch();
|
|
42
43
|
}, [refetch]);
|
|
@@ -59,6 +60,6 @@ export function useConversations(userId) {
|
|
|
59
60
|
return undefined;
|
|
60
61
|
}
|
|
61
62
|
}, [userId, refetch]);
|
|
62
|
-
const totalUnread = conversations.reduce((sum, c) => sum + (c
|
|
63
|
+
const totalUnread = conversations.reduce((sum, c) => sum + (c[unreadField] ?? 0), 0);
|
|
63
64
|
return { conversations, isLoading, error, totalUnread, refetch };
|
|
64
65
|
}
|
|
@@ -7,6 +7,16 @@
|
|
|
7
7
|
import type { OrderDocument } from "../schemas";
|
|
8
8
|
import type { TrackingInfo } from "../../../contracts/shipping";
|
|
9
9
|
export declare function cancelOrderForUser(userId: string, orderId: string, reason: string): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Cancels a subset of line items on an already-placed order and lets the rest
|
|
12
|
+
* proceed. Matches today's whole-order cancellation behavior: marks the items
|
|
13
|
+
* cancelled + sets `refundPending: true`, leaves the actual refund to the
|
|
14
|
+
* existing admin-initiated flow (no auto-refund call here). If every item on
|
|
15
|
+
* the order ends up cancelled, falls through to the same whole-order
|
|
16
|
+
* `cancelOrder` path `cancelOrderForUser` uses, rather than leaving a
|
|
17
|
+
* zero-item live order.
|
|
18
|
+
*/
|
|
19
|
+
export declare function cancelOrderItemsForUser(userId: string, orderId: string, itemIds: string[], reason: string): Promise<void>;
|
|
10
20
|
export declare function listOrdersForUser(userId: string): Promise<OrderDocument[]>;
|
|
11
21
|
export declare function getOrderByIdForUser(userId: string, orderId: string): Promise<OrderDocument>;
|
|
12
22
|
/**
|
|
@@ -8,20 +8,73 @@ import { NotFoundError, AuthorizationError, ValidationError, } from "../../../er
|
|
|
8
8
|
import { serverLogger } from "../../../monitoring";
|
|
9
9
|
import { getProviders } from "../../../contracts";
|
|
10
10
|
import { orderRepository } from "../repository/orders.repository";
|
|
11
|
+
import { ORDER_CANCELLABLE_STATUSES } from "../../../_internal/shared/features/orders/config";
|
|
11
12
|
const ERR_ORDER_NOT_FOUND = "Order not found";
|
|
12
|
-
|
|
13
|
+
function assertCancellable(order) {
|
|
14
|
+
if (!ORDER_CANCELLABLE_STATUSES.includes(order.status)) {
|
|
15
|
+
throw new ValidationError("Only pending or confirmed orders can be cancelled");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
13
18
|
export async function cancelOrderForUser(userId, orderId, reason) {
|
|
14
19
|
const order = await orderRepository.findById(orderId);
|
|
15
20
|
if (!order)
|
|
16
21
|
throw new NotFoundError(ERR_ORDER_NOT_FOUND);
|
|
17
22
|
if (order.userId !== userId)
|
|
18
23
|
throw new AuthorizationError("You are not authorised to cancel this order");
|
|
19
|
-
|
|
20
|
-
throw new ValidationError("Only pending or confirmed orders can be cancelled");
|
|
21
|
-
}
|
|
24
|
+
assertCancellable(order);
|
|
22
25
|
await orderRepository.cancelOrder(orderId, reason);
|
|
23
26
|
serverLogger.info("Order cancelled by user", { userId, orderId, reason });
|
|
24
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* Cancels a subset of line items on an already-placed order and lets the rest
|
|
30
|
+
* proceed. Matches today's whole-order cancellation behavior: marks the items
|
|
31
|
+
* cancelled + sets `refundPending: true`, leaves the actual refund to the
|
|
32
|
+
* existing admin-initiated flow (no auto-refund call here). If every item on
|
|
33
|
+
* the order ends up cancelled, falls through to the same whole-order
|
|
34
|
+
* `cancelOrder` path `cancelOrderForUser` uses, rather than leaving a
|
|
35
|
+
* zero-item live order.
|
|
36
|
+
*/
|
|
37
|
+
export async function cancelOrderItemsForUser(userId, orderId, itemIds, reason) {
|
|
38
|
+
if (itemIds.length === 0) {
|
|
39
|
+
return cancelOrderForUser(userId, orderId, reason);
|
|
40
|
+
}
|
|
41
|
+
const order = await orderRepository.findById(orderId);
|
|
42
|
+
if (!order)
|
|
43
|
+
throw new NotFoundError(ERR_ORDER_NOT_FOUND);
|
|
44
|
+
if (order.userId !== userId)
|
|
45
|
+
throw new AuthorizationError("You are not authorised to cancel this order");
|
|
46
|
+
assertCancellable(order);
|
|
47
|
+
const items = order.items ?? [];
|
|
48
|
+
const targetIds = new Set(itemIds);
|
|
49
|
+
const matchedIds = new Set(items.filter((i) => targetIds.has(i.productId)).map((i) => i.productId));
|
|
50
|
+
if (matchedIds.size === 0) {
|
|
51
|
+
throw new ValidationError("None of the selected items belong to this order");
|
|
52
|
+
}
|
|
53
|
+
const allCancelled = items.every((i) => matchedIds.has(i.productId) || i.cancelledQuantity != null);
|
|
54
|
+
if (allCancelled) {
|
|
55
|
+
return cancelOrderForUser(userId, orderId, reason);
|
|
56
|
+
}
|
|
57
|
+
const cancelledAt = new Date();
|
|
58
|
+
const updatedItems = items.map((item) => matchedIds.has(item.productId)
|
|
59
|
+
? {
|
|
60
|
+
...item,
|
|
61
|
+
cancelledQuantity: item.quantity,
|
|
62
|
+
cancelledAt,
|
|
63
|
+
cancelledReason: reason,
|
|
64
|
+
}
|
|
65
|
+
: item);
|
|
66
|
+
await orderRepository.update(orderId, {
|
|
67
|
+
items: updatedItems,
|
|
68
|
+
refundPending: true,
|
|
69
|
+
updatedAt: cancelledAt,
|
|
70
|
+
});
|
|
71
|
+
serverLogger.info("Order items cancelled by user", {
|
|
72
|
+
userId,
|
|
73
|
+
orderId,
|
|
74
|
+
itemIds: [...matchedIds],
|
|
75
|
+
reason,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
25
78
|
export async function listOrdersForUser(userId) {
|
|
26
79
|
return orderRepository.findByUser(userId);
|
|
27
80
|
}
|
|
@@ -53,5 +53,5 @@ export function MarketplaceOrderCard({ order, links, labels, className = "", var
|
|
|
53
53
|
const actionsRow = (_jsxs(Row, { align: "center", gap: "sm", wrap: true, children: [(isShipped || isDelivered) &&
|
|
54
54
|
trackHref &&
|
|
55
55
|
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 })] }));
|
|
56
|
-
return (_jsxs(Div, { rounded: "xl", border: "subtle", shadow: "hover-md", className: `group relative overflow-hidden bg-[var(--appkit-color-surface)] transition-all duration-200 ${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" })), isListVariant ? (_jsxs(Row, { align: "center", justify: "between", gap: "md", padding: "md", "data-section": "marketplaceordercard-div-412", children: [_jsx(Div, { layout: "flex", align: "start", gap: "3", paddingX: selectable ? "l-xl" : undefined, className: "min-w-0 flex-1", "data-section": "marketplaceordercard-div-413", children: infoBlock }), _jsxs(Row, { align: "center", gap: "md", className: "flex-shrink-0", "data-section": "marketplaceordercard-div-416", children: [_jsx(Row, { align: "center", gap: "md", "data-section": "marketplaceordercard-div-417", children: statusPriceBlock }), actionsRow] })] })) : (_jsxs(Stack, { gap: "md", padding: "md", "data-section": "marketplaceordercard-div-412", children: [_jsx(Div, { layout: "flex", align: "start", gap: "3", paddingX: selectable ? "l-xl" : undefined, "data-section": "marketplaceordercard-div-413", children: infoBlock }), _jsxs(Stack, { gap: "3", "data-section": "marketplaceordercard-div-416", children: [_jsx(Row, { wrap: true, align: "center", justify: "between", gap: "3", "data-section": "marketplaceordercard-div-417", children: statusPriceBlock }), actionsRow] })] }))] }));
|
|
56
|
+
return (_jsxs(Div, { rounded: "xl", border: "subtle", shadow: "hover-md", className: `group relative overflow-hidden bg-[var(--appkit-color-surface)] transition-all duration-200 ${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, onTouchCancel: onSelect && !isSelected ? longPress.onTouchCancel : 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" })), isListVariant ? (_jsxs(Row, { align: "center", justify: "between", gap: "md", padding: "md", "data-section": "marketplaceordercard-div-412", children: [_jsx(Div, { layout: "flex", align: "start", gap: "3", paddingX: selectable ? "l-xl" : undefined, className: "min-w-0 flex-1", "data-section": "marketplaceordercard-div-413", children: infoBlock }), _jsxs(Row, { align: "center", gap: "md", className: "flex-shrink-0", "data-section": "marketplaceordercard-div-416", children: [_jsx(Row, { align: "center", gap: "md", "data-section": "marketplaceordercard-div-417", children: statusPriceBlock }), actionsRow] })] })) : (_jsxs(Stack, { gap: "md", padding: "md", "data-section": "marketplaceordercard-div-412", children: [_jsx(Div, { layout: "flex", align: "start", gap: "3", paddingX: selectable ? "l-xl" : undefined, "data-section": "marketplaceordercard-div-413", children: infoBlock }), _jsxs(Stack, { gap: "3", "data-section": "marketplaceordercard-div-416", children: [_jsx(Row, { wrap: true, align: "center", justify: "between", gap: "3", "data-section": "marketplaceordercard-div-417", children: statusPriceBlock }), actionsRow] })] }))] }));
|
|
57
57
|
}
|
|
@@ -96,6 +96,13 @@ export interface OrderDocumentItem {
|
|
|
96
96
|
prizeRevealDeadline?: string;
|
|
97
97
|
/** SB8-F â€" set after the reveal API picks a winner. */
|
|
98
98
|
revealedItemNumber?: number;
|
|
99
|
+
/** Set when the buyer cancels this specific line item post-order (partial cancellation). */
|
|
100
|
+
cancelledQuantity?: number;
|
|
101
|
+
cancelledAt?: Date;
|
|
102
|
+
cancelledReason?: string;
|
|
103
|
+
/** P-8 GST — snapshotted from the product at order time so the invoice stays accurate even if the product's HSN/rate later changes. */
|
|
104
|
+
hsnCode?: string;
|
|
105
|
+
gstRate?: 0 | 5 | 12 | 18 | 28;
|
|
99
106
|
}
|
|
100
107
|
/**
|
|
101
108
|
* One refund event on an order. Multiple partial refund events are allowed
|
|
@@ -173,6 +180,15 @@ export interface OrderDocument extends BaseDocument {
|
|
|
173
180
|
codRemainingAmount?: number;
|
|
174
181
|
/** COD handling fee charged to the buyer: max(codHandlingFeeMinInPaise, subtotal × codHandlingFeePercent / 100). Only set when paymentMethod === "cod". */
|
|
175
182
|
codHandlingFee?: number;
|
|
183
|
+
/** Order subtotal before GST — the amount GST is computed on. */
|
|
184
|
+
taxableAmount?: number;
|
|
185
|
+
/** Total GST charged (cgst + sgst, or igst — never both pairs at once). */
|
|
186
|
+
gstAmount?: number;
|
|
187
|
+
/** Intra-state half: buyer and seller/store pickup address share the same state. */
|
|
188
|
+
cgst?: number;
|
|
189
|
+
sgst?: number;
|
|
190
|
+
/** Inter-state: buyer and seller/store pickup address are in different states. */
|
|
191
|
+
igst?: number;
|
|
176
192
|
emiEnabled?: boolean;
|
|
177
193
|
/** Number of monthly installments the buyer chose (2–6). */
|
|
178
194
|
emiTenureMonths?: number;
|
|
@@ -36,6 +36,8 @@ export interface OrderItem {
|
|
|
36
36
|
prizeRevealDeadline?: string;
|
|
37
37
|
/** Set after the reveal endpoint picks a winner — the prize item index. */
|
|
38
38
|
revealedItemNumber?: number;
|
|
39
|
+
/** Set when the buyer cancels this specific line item post-order (partial cancellation). */
|
|
40
|
+
cancelledQuantity?: number;
|
|
39
41
|
/**
|
|
40
42
|
* SB-UNI-4 2026-05-13 — bundle identifier when this order line represents a
|
|
41
43
|
* bundle purchase. `productId` then points at the bundle category id and
|
|
@@ -57,7 +57,7 @@ export function MarketplacePreorderCard({ product, className = "", variant = "gr
|
|
|
57
57
|
// The hook rolls back optimistic state when the mutation fails.
|
|
58
58
|
}
|
|
59
59
|
}, [toggle]);
|
|
60
|
-
return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: detailHref, className: "absolute inset-0 block", children: _jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "card", fallback: "Product image" }) }), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { layout: "inline-flex", color: "inverse", size: "xs", weight: "medium", className: "bg-cobalt", rounded: "full", padding: "pill-xs", children: mergedLabels.preOrderBadge }), product.featured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_PREORDER_BADGE, children: mergedLabels.featuredBadge }))] }), _jsxs(Stack, { className: "absolute left-2 top-2 pointer-events-none", gap: "xs", children: [product.partOfBundleIds && product.partOfBundleIds.length > 0 && (_jsx(Span, { className: CLS_SALE_BADGE, title: product.partOfBundleTitles?.[0] ? `In bundle: ${product.partOfBundleTitles[0]}` : "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: CLS_TRENDING_BADGE, title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.sublistingCategoryId && (_jsx(Span, { className: CLS_LIMITED_BADGE, title: "Has variants or sub-listings", children: "Has Variants" }))] }), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
|
|
60
|
+
return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: detailHref, className: "absolute inset-0 block", children: _jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "card", fallback: "Product image" }) }), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { layout: "inline-flex", color: "inverse", size: "xs", weight: "medium", className: "bg-cobalt", rounded: "full", padding: "pill-xs", children: mergedLabels.preOrderBadge }), product.featured && (_jsx(Span, { size: "xs", weight: "medium", className: CLS_PREORDER_BADGE, children: mergedLabels.featuredBadge }))] }), _jsxs(Stack, { className: "absolute left-2 top-2 pointer-events-none", gap: "xs", children: [product.partOfBundleIds && product.partOfBundleIds.length > 0 && (_jsx(Span, { className: CLS_SALE_BADGE, title: product.partOfBundleTitles?.[0] ? `In bundle: ${product.partOfBundleTitles[0]}` : "In a bundle", children: "Bundled" })), product.groupId && (_jsx(Span, { className: CLS_TRENDING_BADGE, title: product.groupTitle ? `Part of set: ${product.groupTitle}` : "Part of a set", children: product.isGroupParent ? "Set Parent" : "In Set" })), product.sublistingCategoryId && (_jsx(Span, { className: CLS_LIMITED_BADGE, title: "Has variants or sub-listings", children: "Has Variants" }))] }), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
|
|
61
61
|
event.stopPropagation();
|
|
62
62
|
onSelect(product.id, !isSelected);
|
|
63
63
|
}, className: selectable || isSelected ? "opacity-100" : "opacity-0 group-hover:opacity-100 transition-opacity" }))] }), _jsx(BaseListingCard.Info, { variant: variant, children: variant === "list" ? (
|
|
@@ -46,7 +46,7 @@ export function MarketplaceBundleCard({ bundle, className = "", variant = "grid"
|
|
|
46
46
|
}
|
|
47
47
|
router.push(String(detailHref));
|
|
48
48
|
}, [detailHref, onNavigate, router]);
|
|
49
|
-
return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: showCollage ? (_jsx(Div, { layout: "grid", className: `h-full w-full gap-[0.125rem] bg-[var(--appkit-color-surface-muted)] ${collageTiles.length === 2 ? "grid-cols-2 grid-rows-1" : "grid-cols-2 grid-rows-2"}`, children: collageTiles.map((tile, i) => (_jsxs(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: [_jsx(MediaImage, { src: tile.imageURL, alt: tile.title ?? `${bundle.name} item ${i + 1}`, size: "card" }), i === collageTiles.length - 1 && overflow > 0 && (_jsxs(Row, { textWeight: "semibold", textSize: "sm", surface: "overlay-md", className: "absolute inset-0 text-white", align: "center", justify: "center", children: ["+", overflow] }))] }, `${tile.productId}-${i}`))) })) : cover ? (_jsx(Div, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "card" }) })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })) }), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { size: "xs", weight: "medium", className: CLS_BUNDLE_PILL, children: mergedLabels.bundleBadge }), _jsx(Span, { layout: "inline-flex", color: "inverse", className: `${stock === "out_of_stock" ? "bg-neutral-500 " : CLS_STOCK_OK}`, rounded: "full", padding: "pill-xs", size: "xs", weight: "medium", children: stock === "out_of_stock" ? mergedLabels.outOfStockBadge : mergedLabels.inStockBadge })] }), stock === "out_of_stock" && (_jsx(Row, { surface: "overlay-xs", className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { color: "inverse", size: "xs", weight: "bold", className: "bg-neutral-900/80 tracking-wider", rounded: "default", padding: "pill-md", transform: "uppercase", children: mergedLabels.outOfStockBadge }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
|
|
49
|
+
return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: showCollage ? (_jsx(Div, { layout: "grid", className: `h-full w-full gap-[0.125rem] bg-[var(--appkit-color-surface-muted)] ${collageTiles.length === 2 ? "grid-cols-2 grid-rows-1" : "grid-cols-2 grid-rows-2"}`, children: collageTiles.map((tile, i) => (_jsxs(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: [_jsx(MediaImage, { src: tile.imageURL, alt: tile.title ?? `${bundle.name} item ${i + 1}`, size: "card" }), i === collageTiles.length - 1 && overflow > 0 && (_jsxs(Row, { textWeight: "semibold", textSize: "sm", surface: "overlay-md", className: "absolute inset-0 text-white", align: "center", justify: "center", children: ["+", overflow] }))] }, `${tile.productId}-${i}`))) })) : cover ? (_jsx(Div, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "card" }) })) : (_jsx(Row, { className: "absolute inset-0 bg-[var(--appkit-color-surface-muted)]", align: "center", justify: "center", children: _jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "xs", children: "No image" }) })) }), _jsxs(Stack, { className: "absolute right-2 top-2", align: "end", gap: "xs", children: [_jsx(Span, { size: "xs", weight: "medium", className: CLS_BUNDLE_PILL, children: mergedLabels.bundleBadge }), _jsx(Span, { layout: "inline-flex", color: "inverse", className: `${stock === "out_of_stock" ? "bg-neutral-500 " : CLS_STOCK_OK}`, rounded: "full", padding: "pill-xs", size: "xs", weight: "medium", children: stock === "out_of_stock" ? mergedLabels.outOfStockBadge : mergedLabels.inStockBadge })] }), stock === "out_of_stock" && (_jsx(Row, { surface: "overlay-xs", className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { color: "inverse", size: "xs", weight: "bold", className: "bg-neutral-900/80 tracking-wider", rounded: "default", padding: "pill-md", transform: "uppercase", children: mergedLabels.outOfStockBadge }) })), onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (event) => {
|
|
50
50
|
event.stopPropagation();
|
|
51
51
|
onSelect(bundle.id, !isSelected);
|
|
52
52
|
}, className: selectable || isSelected
|
|
@@ -89,7 +89,7 @@ export function MarketplacePrizeDrawCard({ product, className = "", variant = "g
|
|
|
89
89
|
}
|
|
90
90
|
router.push(String(detailHref));
|
|
91
91
|
}, [detailHref, onNavigate, router]);
|
|
92
|
-
return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: thumbItems.length > 0 ? (_jsx(Div, { layout: "grid", className: "grid-cols-2 grid-rows-2 gap-[0.125rem] h-full w-full bg-[var(--appkit-color-surface-muted)]", children: Array.from({ length: 4 }).map((_, i) => {
|
|
92
|
+
return (_jsxs(BaseListingCard, { isSelected: isSelected, variant: variant, className: className, onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [_jsxs(BaseListingCard.Hero, { aspect: "square", variant: variant, children: [_jsx(TextLink, { href: String(detailHref), className: "absolute inset-0 block", children: thumbItems.length > 0 ? (_jsx(Div, { layout: "grid", className: "grid-cols-2 grid-rows-2 gap-[0.125rem] h-full w-full bg-[var(--appkit-color-surface-muted)]", children: Array.from({ length: 4 }).map((_, i) => {
|
|
93
93
|
const it = thumbItems[i];
|
|
94
94
|
const img = it?.images?.[0];
|
|
95
95
|
return (_jsx(Div, { className: "relative overflow-hidden bg-[var(--appkit-color-surface-muted)]", children: img ? (_jsx(MediaImage, { src: img, alt: it?.title ?? `Prize ${i + 1}`, size: "card" })) : null }, `thumb-${i}`));
|
|
@@ -6,4 +6,4 @@ export interface PrizeDrawsSectionProps {
|
|
|
6
6
|
* Renders a prize-draws strip on the homepage.
|
|
7
7
|
* W1-38 (2026-05-23): fetches active prize-draw listings from productRepository.
|
|
8
8
|
*/
|
|
9
|
-
export declare function PrizeDrawsSection({ config, }: PrizeDrawsSectionProps): Promise<import("react").JSX.Element>;
|
|
9
|
+
export declare function PrizeDrawsSection({ config, }: PrizeDrawsSectionProps): Promise<import("react").JSX.Element | null>;
|