@mohasinac/appkit 4.9.4 → 4.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +7 -2
- package/dist/_internal/client/features/layout/SidebarCollapseToggle.d.ts +5 -1
- package/dist/_internal/client/features/layout/SidebarCollapseToggle.js +8 -2
- package/dist/_internal/client/features/layout/sidebarPositionClasses.d.ts +20 -0
- package/dist/_internal/client/features/layout/sidebarPositionClasses.js +20 -0
- package/dist/_internal/client/hand-mode/HandModeProvider.d.ts +21 -0
- package/dist/_internal/client/hand-mode/HandModeProvider.js +81 -0
- package/dist/_internal/client/hand-mode/index.d.ts +2 -0
- package/dist/_internal/client/hand-mode/index.js +1 -0
- package/dist/_internal/server/features/auctions/actions.js +3 -2
- package/dist/_internal/server/features/auctions/service.d.ts +4 -3
- package/dist/_internal/server/features/auctions/service.js +6 -6
- package/dist/_internal/server/features/bundles/data.d.ts +9 -0
- package/dist/_internal/server/features/bundles/data.js +14 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/catalogue/og.d.ts +20 -0
- package/dist/_internal/server/features/catalogue/og.js +27 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +43 -0
- package/dist/_internal/server/features/checkout/actions.js +172 -81
- package/dist/_internal/server/features/checkout/index.d.ts +1 -1
- package/dist/_internal/server/features/checkout/index.js +1 -1
- package/dist/_internal/server/features/grouped/data.d.ts +12 -0
- package/dist/_internal/server/features/grouped/data.js +26 -1
- package/dist/_internal/server/features/grouped/index.d.ts +1 -1
- package/dist/_internal/server/features/grouped/index.js +1 -1
- package/dist/_internal/server/features/products/data.d.ts +23 -0
- package/dist/_internal/server/features/products/data.js +159 -0
- package/dist/_internal/server/features/products/index.d.ts +1 -1
- package/dist/_internal/server/features/products/index.js +1 -1
- package/dist/_internal/server/features/seo/robots.js +3 -2
- package/dist/_internal/server/features/seo/sitemap.js +70 -5
- package/dist/_internal/shared/config/schema.d.ts +1 -0
- package/dist/_internal/shared/features/auctions/config.d.ts +20 -0
- package/dist/_internal/shared/features/auctions/config.js +31 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/art/config.js +1 -1
- package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/classified/config.js +2 -2
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/digital-code/config.js +2 -2
- package/dist/_internal/shared/listing-types/live/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/live/config.js +2 -2
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -1
- package/dist/_internal/shared/listing-types/stickers/config.js +1 -1
- package/dist/client.d.ts +4 -6
- package/dist/client.js +10 -4
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/features/about/components/AboutView.js +2 -2
- package/dist/features/about/components/DeveloperView.js +2 -2
- package/dist/features/about/schemas/firestore.d.ts +1 -0
- package/dist/features/account/components/UserSidebar.js +8 -2
- package/dist/features/account/hooks/useProfile.d.ts +2 -0
- package/dist/features/admin/components/AdminSidebar.js +8 -2
- package/dist/features/admin/components/AdminSiteSettingsView.js +12 -4
- package/dist/features/admin/schemas/firestore.d.ts +9 -0
- package/dist/features/admin/schemas/firestore.js +6 -0
- package/dist/features/auctions/actions/bid-actions.js +4 -1
- package/dist/features/auctions/components/AuctionBottomActions.d.ts +2 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +14 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +21 -9
- package/dist/features/auctions/components/LiveMinIncrement.d.ts +18 -0
- package/dist/features/auctions/components/LiveMinIncrement.js +17 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +5 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +7 -3
- package/dist/features/auth/schemas/firestore.d.ts +3 -0
- package/dist/features/cart/components/CartDrawer.js +4 -1
- package/dist/features/catalogue/components/PublicCatalogueItemDetailView.d.ts +11 -0
- package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +33 -0
- package/dist/features/catalogue/components/PublicCatalogueView.js +2 -1
- package/dist/features/catalogue/index.d.ts +2 -0
- package/dist/features/catalogue/index.js +1 -0
- package/dist/features/categories/components/BundleDetailView.d.ts +3 -1
- package/dist/features/categories/components/BundleDetailView.js +3 -2
- package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +12 -0
- package/dist/features/classified/components/ClassifiedContactSellerPanel.js +49 -0
- package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +7 -0
- package/dist/features/classified/components/ClassifiedDetailPageView.js +69 -0
- package/dist/features/contact/components/ContactPageView.js +8 -2
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.d.ts +19 -0
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +79 -0
- package/dist/features/grouped/components/GroupedListingsCarousel.d.ts +13 -0
- package/dist/features/grouped/components/GroupedListingsCarousel.js +31 -0
- package/dist/features/grouped/repository/grouped-listings.repository.d.ts +8 -0
- package/dist/features/grouped/repository/grouped-listings.repository.js +16 -0
- package/dist/features/homepage/components/BrandsSection.js +3 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +12 -1
- package/dist/features/homepage/components/GoogleReviewsSection.js +2 -2
- package/dist/features/homepage/components/HeroCarousel.js +3 -1
- package/dist/features/homepage/components/SectionCarousel.js +5 -2
- package/dist/features/homepage/components/SocialFeedSection.js +2 -2
- package/dist/features/homepage/components/WhatsAppCommunitySection.js +3 -1
- package/dist/features/layout/BackToTop.js +3 -1
- package/dist/features/layout/BottomActions.js +4 -3
- package/dist/features/layout/BottomActionsContext.d.ts +10 -0
- package/dist/features/layout/BottomActionsContext.js +5 -1
- package/dist/features/layout/SidebarLayout.js +7 -1
- package/dist/features/layout/hooks/useBottomActions.d.ts +2 -0
- package/dist/features/layout/hooks/useBottomActions.js +4 -1
- package/dist/features/live/components/LiveItemDetailPageView.d.ts +9 -0
- package/dist/features/live/components/LiveItemDetailPageView.js +74 -0
- package/dist/features/media/MediaVideo.js +17 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +9 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +11 -1
- package/dist/features/products/components/ProductDetailPageView.js +11 -116
- package/dist/features/products/components/RelatedItemsSection.d.ts +16 -0
- package/dist/features/products/components/RelatedItemsSection.js +17 -0
- package/dist/features/seller/components/SellerProductShell.js +1 -1
- package/dist/features/seller/components/SellerSidebar.js +8 -2
- package/dist/features/shell/QuickFormDrawer.js +3 -1
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +114 -3
- package/dist/hand-mode/index.d.ts +12 -0
- package/dist/hand-mode/index.js +11 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +7 -0
- package/dist/react/contexts/SessionContext.d.ts +5 -0
- package/dist/react/contexts/SessionContext.js +2 -0
- package/dist/seed/bids-seed-data.js +48 -3
- package/dist/seed/grouped-listings-seed-data.js +167 -0
- package/dist/seed/payouts-seed-data.js +4 -4
- package/dist/seed/products-art-seed-data.js +188 -5
- package/dist/seed/products-auctions-seed-data.js +215 -6
- package/dist/seed/products-classifieds-seed-data.js +255 -8
- package/dist/seed/products-digital-codes-seed-data.js +258 -19
- package/dist/seed/products-live-items-seed-data.js +143 -10
- package/dist/seed/products-preorders-seed-data.js +213 -8
- package/dist/seed/products-prize-draws-seed-data.js +198 -0
- package/dist/seed/products-standard-seed-data.js +141 -0
- package/dist/seed/products-stickers-seed-data.js +178 -5
- package/dist/seed/site-settings-seed-data.js +13 -31
- package/dist/seed/store-extensions-seed-data.js +1 -1
- package/dist/server-entry.d.ts +5 -2
- package/dist/server-entry.js +5 -2
- package/dist/server.d.ts +4 -2
- package/dist/server.js +4 -2
- package/dist/styles.css +30 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Drawer.js +7 -4
- package/dist/ui/components/FilterDrawer.js +1 -1
- package/dist/ui/components/HandModeRow.d.ts +12 -0
- package/dist/ui/components/HandModeRow.js +18 -0
- package/dist/ui/components/ImageLightbox.js +37 -3
- package/dist/ui/components/ImageLightbox.style.css +28 -0
- package/dist/ui/components/Layout.d.ts +5 -1
- package/dist/ui/components/Layout.js +2 -1
- package/dist/ui/components/Layout.style.css +1 -0
- package/dist/ui/components/ListingFilterDrawer.js +3 -1
- package/dist/ui/components/Modal.js +3 -1
- package/dist/ui/components/SideDrawer.js +8 -5
- package/dist/ui/components/SideModal.js +7 -4
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/utils/media-url.d.ts +9 -0
- package/dist/utils/media-url.js +39 -0
- package/package.json +1 -1
|
@@ -33,6 +33,7 @@ import { DASHBOARD_DESKTOP_MEDIA_QUERY } from "../../../shared/features/layout/c
|
|
|
33
33
|
import { filterNavItems } from "./filterNavItems";
|
|
34
34
|
import { useSiteSettings } from "../../../../core/hooks/useSiteSettings";
|
|
35
35
|
import { useTheme } from "../../theme";
|
|
36
|
+
import { useHandMode } from "../../hand-mode";
|
|
36
37
|
import { BackgroundRenderer, Div, Nav, Span } from "../../../../ui";
|
|
37
38
|
import { useToast } from "../../../../ui/components/Toast";
|
|
38
39
|
import { useVisualViewportInset } from "../../../../react/hooks/useVisualViewportInset";
|
|
@@ -149,7 +150,10 @@ function DashboardBottomNav({ items, activeHref }) {
|
|
|
149
150
|
return (_jsxs(Link, { href: item.href, className: `flex flex-1 flex-col items-center justify-center gap-[var(--appkit-space-0-5)] py-[var(--appkit-space-2)] ${isActive ? "text-[var(--appkit-color-primary)]" : "text-[var(--appkit-color-text-muted)]"}`, "aria-current": isActive ? "page" : undefined, children: [item.icon && _jsx(Span, { size: "base", children: item.icon }), _jsx(Span, { size: "xs", className: "max-w-full truncate", children: item.label })] }, item.href));
|
|
150
151
|
}) }));
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
+
// Asymmetric padding clears the persistent sidebar rail — right-heavy by
|
|
154
|
+
// default (rail docks left), left-heavy in left-hand mode (rail docks right).
|
|
155
|
+
const DEFAULT_CONTENT_PADDING_RIGHT = "px-[var(--appkit-space-5)] py-[var(--appkit-space-8)] lg:pl-14 lg:pr-6 xl:pl-16 xl:pr-10";
|
|
156
|
+
const DEFAULT_CONTENT_PADDING_LEFT = "px-[var(--appkit-space-5)] py-[var(--appkit-space-8)] lg:pr-14 lg:pl-6 xl:pr-16 xl:pl-10";
|
|
153
157
|
const DEFAULT_CONTENT_MAX_WIDTH = "max-w-screen-2xl";
|
|
154
158
|
export function DashboardLayoutClient({ variant, groups, permissions, activeHref: explicitActiveHref, responsive: _responsive, crossNav, className, contentPadding, contentSurface, contentMaxWidth, lightBackground, darkBackground, children, }) {
|
|
155
159
|
const pathname = usePathname();
|
|
@@ -178,6 +182,7 @@ export function DashboardLayoutClient({ variant, groups, permissions, activeHref
|
|
|
178
182
|
const { data: settings } = useSiteSettings();
|
|
179
183
|
const navConfig = settings?.navConfig;
|
|
180
184
|
const { theme } = useTheme();
|
|
185
|
+
const { hand } = useHandMode();
|
|
181
186
|
// Explicit props win; otherwise fall back to the same siteSettings.background
|
|
182
187
|
// the public shell reads, so admin/store/user dashboards match it by default.
|
|
183
188
|
const resolvedLightBackground = lightBackground ?? settings?.background?.light;
|
|
@@ -193,7 +198,7 @@ export function DashboardLayoutClient({ variant, groups, permissions, activeHref
|
|
|
193
198
|
return (_jsxs(_Fragment, { children: [hasBackground && (_jsx(BackgroundRenderer, { mode: theme === "dark" ? "dark" : "light", lightMode: resolvedLightBackground ?? { type: "color", value: "" }, darkMode: resolvedDarkBackground ?? { type: "color", value: "" } })), variant === "admin" && (_jsx(AdminSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activePath: activeHref, groups: adminGroups, onCloseMobile: close, onToggle: toggle, className: className, renderFooter: renderCrossNavFooter })), variant === "store" && (_jsx(StoreSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, activeHref: activeHref, items: [], groups: filteredGroups, onCloseMobile: close, onToggle: toggle, className: className, renderFooter: renderCrossNavFooter })), variant === "user" && (_jsx(UserSidebar, { variant: "sidebar", desktopOpen: desktopOpen, mobileOpen: mobileOpen, items: filteredGroups.flatMap((g) => g.items), groups: filteredGroups, onCloseMobile: close, onToggle: toggle, className: className, renderFooter: renderCrossNavFooter })), _jsx(Div, { className: [
|
|
194
199
|
"w-full flex-1 flex flex-col min-h-[calc(100dvh-var(--header-height,3.5rem))]",
|
|
195
200
|
"pb-[var(--appkit-space-16)] lg:pb-[0]", // clears the fixed mobile bottom-nav
|
|
196
|
-
contentPadding ??
|
|
201
|
+
contentPadding ?? (hand === "left" ? DEFAULT_CONTENT_PADDING_LEFT : DEFAULT_CONTENT_PADDING_RIGHT),
|
|
197
202
|
contentSurface,
|
|
198
203
|
].filter(Boolean).join(" "), children: _jsx(Div, { className: ["w-full flex-1 mx-auto", contentMaxWidth ?? DEFAULT_CONTENT_MAX_WIDTH].filter(Boolean).join(" "), children: children }) }), _jsx(DashboardBottomNav, { items: filteredGroups.flatMap((g) => g.items), activeHref: activeHref })] }));
|
|
199
204
|
}
|
|
@@ -15,5 +15,9 @@ export interface SidebarCollapseToggleProps {
|
|
|
15
15
|
/** aria-label override; defaults to "Collapse sidebar" / "Expand sidebar". */
|
|
16
16
|
expandedLabel?: string;
|
|
17
17
|
collapsedLabel?: string;
|
|
18
|
+
/** Which edge the parent sidebar panel is docked to. Defaults to "right"
|
|
19
|
+
* (the handle protrudes to the panel's outer/right edge). "left" mirrors
|
|
20
|
+
* the rounded corner and chevron direction for left-hand mode. */
|
|
21
|
+
edge?: "left" | "right";
|
|
18
22
|
}
|
|
19
|
-
export declare function SidebarCollapseToggle({ expanded, onToggle, expandedLabel, collapsedLabel, }: SidebarCollapseToggleProps): React.JSX.Element;
|
|
23
|
+
export declare function SidebarCollapseToggle({ expanded, onToggle, expandedLabel, collapsedLabel, edge, }: SidebarCollapseToggleProps): React.JSX.Element;
|
|
@@ -7,6 +7,12 @@ const HANDLE_STYLE = {
|
|
|
7
7
|
const CHEVRON_STYLE = {
|
|
8
8
|
color: "var(--appkit-color-text-on-primary)",
|
|
9
9
|
};
|
|
10
|
-
export function SidebarCollapseToggle({ expanded, onToggle, expandedLabel = "Collapse sidebar", collapsedLabel = "Expand sidebar", }) {
|
|
11
|
-
|
|
10
|
+
export function SidebarCollapseToggle({ expanded, onToggle, expandedLabel = "Collapse sidebar", collapsedLabel = "Expand sidebar", edge = "right", }) {
|
|
11
|
+
// The panel's free (open) edge is opposite its dock side: a left-docked
|
|
12
|
+
// panel (edge="left") protrudes its handle to the right, and vice versa.
|
|
13
|
+
const roundedClass = edge === "left" ? "rounded-r-[1.25rem]" : "rounded-l-[1.25rem]";
|
|
14
|
+
// Chevron rotation direction mirrors along with the dock side so it keeps
|
|
15
|
+
// pointing "into" the collapsed panel regardless of which edge it's on.
|
|
16
|
+
const chevronRotated = edge === "left" ? !expanded : expanded;
|
|
17
|
+
return (_jsx(IconButton, { type: "button", onClick: onToggle, "aria-label": expanded ? expandedLabel : collapsedLabel, "aria-expanded": expanded, variant: "ghost", style: HANDLE_STYLE, className: `w-9 shrink-0 ${roundedClass} shadow-lg transition-all duration-200 hover:shadow-xl hover:brightness-110 active:scale-[0.96]`, icon: _jsx("svg", { className: `w-4 h-4 drop-shadow-sm transition-transform duration-300 ${chevronRotated ? "rotate-180" : ""}`, style: CHEVRON_STYLE, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M15 19l-7-7 7-7" }) }) }));
|
|
12
18
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { HandMode } from "../../hand-mode";
|
|
2
|
+
/** Classes for the desktop persistent sidebar rail (Admin/Seller/User
|
|
3
|
+
* `variant="sidebar"`) — flips which edge it docks to, its border side, and
|
|
4
|
+
* its collapsed-state translate direction, based on the hand-mode
|
|
5
|
+
* preference. Kept as a single shared helper so the flip logic isn't
|
|
6
|
+
* hand-forked identically across all three sidebar files. */
|
|
7
|
+
export interface SidebarRailClasses {
|
|
8
|
+
edgeClass: string;
|
|
9
|
+
borderClass: string;
|
|
10
|
+
collapsedTranslateClass: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function getSidebarRailClasses(hand: HandMode): SidebarRailClasses;
|
|
13
|
+
/** Classes for the mobile overlay drawer variant (Admin/Seller/User
|
|
14
|
+
* `variant="overlay"` → `DrawerPanel`) — same edge/border flip, no
|
|
15
|
+
* translate state since the panel is conditionally mounted, not animated. */
|
|
16
|
+
export interface SidebarOverlayClasses {
|
|
17
|
+
edgeClass: string;
|
|
18
|
+
borderClass: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function getSidebarOverlayClasses(hand: HandMode): SidebarOverlayClasses;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function getSidebarRailClasses(hand) {
|
|
2
|
+
if (hand === "left") {
|
|
3
|
+
return {
|
|
4
|
+
edgeClass: "left-0",
|
|
5
|
+
borderClass: "border-r",
|
|
6
|
+
collapsedTranslateClass: "-translate-x-[calc(100%-1.25rem)]",
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
edgeClass: "right-0",
|
|
11
|
+
borderClass: "border-l",
|
|
12
|
+
collapsedTranslateClass: "translate-x-[calc(100%-1.25rem)]",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function getSidebarOverlayClasses(hand) {
|
|
16
|
+
if (hand === "left") {
|
|
17
|
+
return { edgeClass: "left-0", borderClass: "border-r" };
|
|
18
|
+
}
|
|
19
|
+
return { edgeClass: "right-0", borderClass: "border-l" };
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type HandMode = "left" | "right";
|
|
3
|
+
export interface HandModeContextValue {
|
|
4
|
+
/** Which side CTA panels/drawers/sidebars dock to and where close buttons sit. */
|
|
5
|
+
hand: HandMode;
|
|
6
|
+
/** Persist a new hand-mode preference and re-render the flipped layout. */
|
|
7
|
+
setHand: (next: HandMode) => void;
|
|
8
|
+
/** Flip preference between right ↔ left. */
|
|
9
|
+
toggleHand: () => void;
|
|
10
|
+
}
|
|
11
|
+
export interface HandModeProviderProps {
|
|
12
|
+
/**
|
|
13
|
+
* Seed value from the signed-in user's server-persisted preference
|
|
14
|
+
* (`user.uiPreferences.handMode`), used only when no value is already
|
|
15
|
+
* cached in localStorage on this device.
|
|
16
|
+
*/
|
|
17
|
+
initialHand?: HandMode;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
export declare function HandModeProvider({ initialHand, children }: HandModeProviderProps): import("react").JSX.Element;
|
|
21
|
+
export declare function useHandMode(): HandModeContextValue;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
4
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
5
|
+
// Brand-neutral localStorage namespace, matching the `appkit:theme-mode` /
|
|
6
|
+
// `appkit:theme-id` convention. audit-ssr-in-appkit blocks hardcoded brand
|
|
7
|
+
// strings inside `_internal/`.
|
|
8
|
+
const HAND_STORAGE_KEY = "appkit:hand-mode";
|
|
9
|
+
// Default (non-null) context value — unlike `ThemeContext` (which throws
|
|
10
|
+
// outside its provider because it's only ever consumed at the app-shell
|
|
11
|
+
// level), `useHandMode()` is called from base UI primitives (SideDrawer,
|
|
12
|
+
// Modal, Drawer, BackToTop, ...) that ship as part of the public design
|
|
13
|
+
// system and must degrade gracefully to the "right" default for any
|
|
14
|
+
// consumer that hasn't mounted `<HandModeProvider>`.
|
|
15
|
+
const DEFAULT_CONTEXT_VALUE = {
|
|
16
|
+
hand: "right",
|
|
17
|
+
setHand: () => { },
|
|
18
|
+
toggleHand: () => { },
|
|
19
|
+
};
|
|
20
|
+
const HandModeContext = createContext(DEFAULT_CONTEXT_VALUE);
|
|
21
|
+
function readHandPreference() {
|
|
22
|
+
if (typeof window === "undefined")
|
|
23
|
+
return "right";
|
|
24
|
+
try {
|
|
25
|
+
const stored = window.localStorage.getItem(HAND_STORAGE_KEY);
|
|
26
|
+
if (stored === "left" || stored === "right")
|
|
27
|
+
return stored;
|
|
28
|
+
}
|
|
29
|
+
catch (_err) {
|
|
30
|
+
void normalizeError(_err); /* localStorage unavailable in privacy/iframe context */
|
|
31
|
+
}
|
|
32
|
+
return "right";
|
|
33
|
+
}
|
|
34
|
+
/** Apply the hand-mode preference to `<html>` via a `data-hand` attribute.
|
|
35
|
+
* Deliberately NOT `dir="rtl"/"ltr"` — that would trigger the browser's
|
|
36
|
+
* bidi text-direction and native-control-mirroring behavior repo-wide, which
|
|
37
|
+
* is not what this "reachability" preference means. `data-hand` is a plain
|
|
38
|
+
* presentational attribute, same category as `data-theme`. */
|
|
39
|
+
function applyHand(hand) {
|
|
40
|
+
if (typeof document === "undefined")
|
|
41
|
+
return;
|
|
42
|
+
document.documentElement.setAttribute("data-hand", hand);
|
|
43
|
+
}
|
|
44
|
+
export function HandModeProvider({ initialHand, children }) {
|
|
45
|
+
const [hand, setHandState] = useState(() => {
|
|
46
|
+
if (typeof window === "undefined")
|
|
47
|
+
return initialHand ?? "right";
|
|
48
|
+
const stored = readHandPreference();
|
|
49
|
+
// readHandPreference() falls back to "right" when nothing is cached —
|
|
50
|
+
// in that case, prefer the server-seeded value over the hardcoded default.
|
|
51
|
+
try {
|
|
52
|
+
if (window.localStorage.getItem(HAND_STORAGE_KEY) === null) {
|
|
53
|
+
return initialHand ?? stored;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (_err) {
|
|
57
|
+
void normalizeError(_err);
|
|
58
|
+
}
|
|
59
|
+
return stored;
|
|
60
|
+
});
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
applyHand(hand);
|
|
63
|
+
}, [hand]);
|
|
64
|
+
const setHand = useCallback((next) => {
|
|
65
|
+
setHandState(next);
|
|
66
|
+
try {
|
|
67
|
+
window.localStorage.setItem(HAND_STORAGE_KEY, next);
|
|
68
|
+
}
|
|
69
|
+
catch (_err) {
|
|
70
|
+
void normalizeError(_err); /* localStorage unavailable; preference applies for this session */
|
|
71
|
+
}
|
|
72
|
+
}, []);
|
|
73
|
+
const toggleHand = useCallback(() => {
|
|
74
|
+
setHand(hand === "left" ? "right" : "left");
|
|
75
|
+
}, [hand, setHand]);
|
|
76
|
+
const value = useMemo(() => ({ hand, setHand, toggleHand }), [hand, setHand, toggleHand]);
|
|
77
|
+
return _jsx(HandModeContext.Provider, { value: value, children: children });
|
|
78
|
+
}
|
|
79
|
+
export function useHandMode() {
|
|
80
|
+
return useContext(HandModeContext);
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { HandModeProvider, useHandMode } from "./HandModeProvider";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use server";
|
|
2
2
|
import { wrapAction } from "@mohasinac/appkit/server";
|
|
3
|
-
import { bidRepository, productRepository, userRepository } from "../../../../repositories";
|
|
3
|
+
import { bidRepository, productRepository, userRepository, siteSettingsRepository } from "../../../../repositories";
|
|
4
4
|
import { requireRoleUser } from "../../../../providers/auth-firebase/helpers";
|
|
5
5
|
import { placeBidSchema } from "../../../shared/features/auctions/schema";
|
|
6
6
|
import { assertAuctionActive, assertBidAmount, assertNotAuctionOwner, shouldAutoExtend, computeExtendedEndDate, } from "./service";
|
|
@@ -14,7 +14,8 @@ export async function placeBidAction(input) {
|
|
|
14
14
|
const { auctionId, amount } = parsed.data;
|
|
15
15
|
const product = await assertAuctionActive(auctionId);
|
|
16
16
|
assertNotAuctionOwner(product, user.uid);
|
|
17
|
-
|
|
17
|
+
const settings = await siteSettingsRepository.getSingleton();
|
|
18
|
+
assertBidAmount(product, amount, settings.auctionConfig?.bidIncrementTiers ?? []);
|
|
18
19
|
const profile = await userRepository.findById(user.uid).catch(() => null);
|
|
19
20
|
const userName = profile?.displayName ?? user.name ?? "Anonymous";
|
|
20
21
|
// Mark all current bids for this auction as outbid, then set new winning bid
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { type BidIncrementTier } from "../../../shared/features/auctions/config";
|
|
1
2
|
import type { ProductDocument } from "../../../shared/features/products/types";
|
|
2
3
|
/** Assert an auction exists, is published, and is still active. Returns the product. */
|
|
3
4
|
export declare function assertAuctionActive(auctionId: string): Promise<ProductDocument>;
|
|
4
|
-
/** Compute the minimum valid bid given the current state. */
|
|
5
|
-
export declare function computeMinBid(product: ProductDocument): number;
|
|
5
|
+
/** Compute the minimum valid bid given the current state and the admin's tier table. */
|
|
6
|
+
export declare function computeMinBid(product: ProductDocument, tiers: BidIncrementTier[]): number;
|
|
6
7
|
/** Assert a bid amount is valid against the current auction state. */
|
|
7
|
-
export declare function assertBidAmount(product: ProductDocument, amount: number): void;
|
|
8
|
+
export declare function assertBidAmount(product: ProductDocument, amount: number, tiers: BidIncrementTier[]): void;
|
|
8
9
|
/** Assert the bidder is not the auction owner. */
|
|
9
10
|
export declare function assertNotAuctionOwner(product: ProductDocument, bidderId: string): void;
|
|
10
11
|
/** Return true if the bid was placed within the sniping window and the auction should auto-extend. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { productRepository } from "../../../../repositories";
|
|
2
2
|
import { isAuctionListing } from "../../../../features/products/utils/listing-type";
|
|
3
3
|
import { AuctionNotFoundError, AuctionEndedError, BidTooLowError, BidOnOwnAuctionError, } from "../../../shared/features/auctions/errors";
|
|
4
|
-
import {
|
|
4
|
+
import { AUCTION_SNIPING_WINDOW_SECONDS, AUCTION_DEFAULT_EXTENSION_MINUTES, resolveMinBidIncrement, } from "../../../shared/features/auctions/config";
|
|
5
5
|
/** Assert an auction exists, is published, and is still active. Returns the product. */
|
|
6
6
|
export async function assertAuctionActive(auctionId) {
|
|
7
7
|
const product = await productRepository.findByIdOrSlug(auctionId).catch(() => null);
|
|
@@ -16,15 +16,15 @@ export async function assertAuctionActive(auctionId) {
|
|
|
16
16
|
throw new AuctionEndedError(auctionId);
|
|
17
17
|
return product;
|
|
18
18
|
}
|
|
19
|
-
/** Compute the minimum valid bid given the current state. */
|
|
20
|
-
export function computeMinBid(product) {
|
|
19
|
+
/** Compute the minimum valid bid given the current state and the admin's tier table. */
|
|
20
|
+
export function computeMinBid(product, tiers) {
|
|
21
21
|
const current = product.currentBid ?? product.startingBid ?? 0;
|
|
22
|
-
const increment = product.minBidIncrement
|
|
22
|
+
const increment = resolveMinBidIncrement(current, tiers, product.minBidIncrement);
|
|
23
23
|
return current + increment;
|
|
24
24
|
}
|
|
25
25
|
/** Assert a bid amount is valid against the current auction state. */
|
|
26
|
-
export function assertBidAmount(product, amount) {
|
|
27
|
-
const minBid = computeMinBid(product);
|
|
26
|
+
export function assertBidAmount(product, amount, tiers) {
|
|
27
|
+
const minBid = computeMinBid(product, tiers);
|
|
28
28
|
if (amount < minBid)
|
|
29
29
|
throw new BidTooLowError(minBid);
|
|
30
30
|
}
|
|
@@ -45,3 +45,12 @@ export declare function resolveBundleOriginalTotal(productIds: string[]): Promis
|
|
|
45
45
|
* `limit` (default 8) and filtered to `isActive: true` server-side.
|
|
46
46
|
*/
|
|
47
47
|
export declare const listFeaturedBundles: (limit?: number, _opts?: BundleDataOptions) => Promise<CategoryDocument[]>;
|
|
48
|
+
/**
|
|
49
|
+
* "Related Bundles" for the bundle detail page — every other active bundle,
|
|
50
|
+
* excluding the current one. Bundles are a CategoryDocument, not a
|
|
51
|
+
* ProductDocument, so this mirrors BrandDetailPageView's "Related Brands"
|
|
52
|
+
* pattern (all active siblings of the same categoryType) rather than the
|
|
53
|
+
* product-based 4-signal computeRelatedItems() used elsewhere — bundle
|
|
54
|
+
* detail pages had no related section at all before this.
|
|
55
|
+
*/
|
|
56
|
+
export declare const getRelatedBundles: (bundle: Pick<CategoryDocument, "id">, limit?: number) => Promise<CategoryDocument[]>;
|
|
@@ -79,3 +79,17 @@ export const listFeaturedBundles = cache(async (limit = 8, _opts) => {
|
|
|
79
79
|
.catch(() => []);
|
|
80
80
|
return all;
|
|
81
81
|
});
|
|
82
|
+
/**
|
|
83
|
+
* "Related Bundles" for the bundle detail page — every other active bundle,
|
|
84
|
+
* excluding the current one. Bundles are a CategoryDocument, not a
|
|
85
|
+
* ProductDocument, so this mirrors BrandDetailPageView's "Related Brands"
|
|
86
|
+
* pattern (all active siblings of the same categoryType) rather than the
|
|
87
|
+
* product-based 4-signal computeRelatedItems() used elsewhere — bundle
|
|
88
|
+
* detail pages had no related section at all before this.
|
|
89
|
+
*/
|
|
90
|
+
export const getRelatedBundles = cache(async (bundle, limit = 8) => {
|
|
91
|
+
const all = await categoriesRepository
|
|
92
|
+
.listByType("bundle", { activeOnly: true, limit: limit + 1 })
|
|
93
|
+
.catch(() => []);
|
|
94
|
+
return all.filter((b) => b.id !== bundle.id).slice(0, limit);
|
|
95
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, type BundleDataOptions, } from "./data";
|
|
1
|
+
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, getRelatedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, type BundleDataOptions, } from "./data";
|
|
2
2
|
export { buildBundleMetadata, type BundleMetadataOptions } from "./metadata";
|
|
3
3
|
export { renderBundleOg, renderBundleOgImage, type BundleOgData, } from "./og";
|
|
4
4
|
export { addBundleToCartAction } from "./actions";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, } from "./data";
|
|
1
|
+
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, getRelatedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, } from "./data";
|
|
2
2
|
export { buildBundleMetadata } from "./metadata";
|
|
3
3
|
// S-SBUNI-4 2026-05-13 — bundle OG renderer.
|
|
4
4
|
export { renderBundleOg, renderBundleOgImage, } from "./og";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
export interface CatalogueItemOgData {
|
|
3
|
+
title: string;
|
|
4
|
+
ownerName?: string | null;
|
|
5
|
+
priceLabel?: string | null;
|
|
6
|
+
imageUrl?: string | null;
|
|
7
|
+
}
|
|
8
|
+
interface CatalogueItemDocLike {
|
|
9
|
+
title?: string | null;
|
|
10
|
+
price?: number | null;
|
|
11
|
+
mainImage?: string | null;
|
|
12
|
+
images?: (string | null | undefined)[] | null;
|
|
13
|
+
}
|
|
14
|
+
export declare function renderCatalogueItemOg(doc: CatalogueItemDocLike | null | undefined, opts: {
|
|
15
|
+
siteName: string;
|
|
16
|
+
baseUrl?: string;
|
|
17
|
+
ownerName?: string | null;
|
|
18
|
+
}): ReactElement;
|
|
19
|
+
export declare function renderCatalogueItemOgImage(data: CatalogueItemOgData, siteName: string): ReactElement;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { renderOgLayout } from "../seo/og-layout";
|
|
2
|
+
import { resolveOgImageUrl } from "../seo/og";
|
|
3
|
+
export function renderCatalogueItemOg(doc, opts) {
|
|
4
|
+
const priceLabel = doc?.price != null && doc.price > 0
|
|
5
|
+
? new Intl.NumberFormat("en-IN", {
|
|
6
|
+
style: "currency",
|
|
7
|
+
currency: "INR",
|
|
8
|
+
maximumFractionDigits: 0,
|
|
9
|
+
}).format(doc.price)
|
|
10
|
+
: null;
|
|
11
|
+
return renderCatalogueItemOgImage({
|
|
12
|
+
title: doc?.title ?? "Catalogue Item",
|
|
13
|
+
ownerName: opts.ownerName,
|
|
14
|
+
priceLabel,
|
|
15
|
+
imageUrl: resolveOgImageUrl(doc?.mainImage || doc?.images?.[0] || null, opts.baseUrl),
|
|
16
|
+
}, opts.siteName);
|
|
17
|
+
}
|
|
18
|
+
export function renderCatalogueItemOgImage(data, siteName) {
|
|
19
|
+
return renderOgLayout({
|
|
20
|
+
title: data.title,
|
|
21
|
+
subtitle: data.ownerName ? `Owned by ${data.ownerName}` : undefined,
|
|
22
|
+
imageUrl: data.imageUrl,
|
|
23
|
+
siteName: `${siteName} · Catalogue`,
|
|
24
|
+
accentSlot: data.priceLabel,
|
|
25
|
+
theme: { accentColor: "#a78bfa" },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -32,6 +32,18 @@ export interface CreateCheckoutOrderInput {
|
|
|
32
32
|
/** Buyer opted into shipment protection at checkout. Defaults false (unchecked). Only actually charged/honored when siteSettings.commissions.shipmentProtectionFeeEnabled is also true. */
|
|
33
33
|
shipmentProtectionAddon?: boolean;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Exported so `/api/payment/create-order` (pre-charge amount) and the
|
|
37
|
+
* read-only `previewCheckoutPricing` (Order Summary display) can compute the
|
|
38
|
+
* exact same per-seller shipping fee that order creation charges/records —
|
|
39
|
+
* a single source of truth instead of a third re-implementation.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveShippingCost(storeId: string | undefined): Promise<{
|
|
42
|
+
shippingFee: number;
|
|
43
|
+
storeOwnerId: string | undefined;
|
|
44
|
+
storeEmiEnabled: boolean;
|
|
45
|
+
storeState: string | undefined;
|
|
46
|
+
}>;
|
|
35
47
|
/**
|
|
36
48
|
* Place order(s) from the user's cart in a single Firestore transaction.
|
|
37
49
|
*
|
|
@@ -39,6 +51,37 @@ export interface CreateCheckoutOrderInput {
|
|
|
39
51
|
* auth + rate-limiting before calling this and supply the resolved user fields.
|
|
40
52
|
*/
|
|
41
53
|
export declare function createCheckoutOrderAction(input: CreateCheckoutOrderInput): Promise<CheckoutOrderResult>;
|
|
54
|
+
export interface CheckoutPricingPreviewInput {
|
|
55
|
+
userId: string;
|
|
56
|
+
/** Omitted for digital-only carts — GST can't be computed without a buyer state. */
|
|
57
|
+
addressId?: string;
|
|
58
|
+
paymentMethod: CheckoutPaymentMethod;
|
|
59
|
+
excludedProductIds?: string[];
|
|
60
|
+
whatsappNotifyAddon?: boolean;
|
|
61
|
+
giftWrapAddon?: boolean;
|
|
62
|
+
shipmentProtectionAddon?: boolean;
|
|
63
|
+
}
|
|
64
|
+
export interface CheckoutPricingPreview {
|
|
65
|
+
subtotal: number;
|
|
66
|
+
shippingFee: number;
|
|
67
|
+
codHandlingFee: number;
|
|
68
|
+
whatsappNotifyFee: number;
|
|
69
|
+
giftWrapFee: number;
|
|
70
|
+
shipmentProtectionFee: number;
|
|
71
|
+
gstAmount: number;
|
|
72
|
+
couponDiscount: number;
|
|
73
|
+
total: number;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Read-only "what will I actually be charged" preview for the checkout
|
|
77
|
+
* Order Summary. Reuses the exact same building blocks
|
|
78
|
+
* createCheckoutOrderAction / verifyAndPlaceRazorpayOrderAction use when an
|
|
79
|
+
* order is actually placed — resolveShippingCost, the compute*Fee helpers,
|
|
80
|
+
* computeGroupCouponDiscount, and the GST calc — so the number shown to the
|
|
81
|
+
* buyer can never diverge from what gets charged/recorded. Never decrements
|
|
82
|
+
* stock, creates an order, or writes coupon usage.
|
|
83
|
+
*/
|
|
84
|
+
export declare function previewCheckoutPricing(input: CheckoutPricingPreviewInput): Promise<CheckoutPricingPreview>;
|
|
42
85
|
/**
|
|
43
86
|
* Mark an order as paid after a payment provider verifies the transaction.
|
|
44
87
|
* Consumers call this from /api/payment/verify or a payment webhook.
|