@pagamio/frontend-commons-lib 0.8.286 → 0.8.288
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/lib/components/ecommerce/BundleCard.d.ts +11 -0
- package/lib/components/ecommerce/BundleCard.js +40 -0
- package/lib/components/ecommerce/CartItemCard.d.ts +11 -0
- package/lib/components/ecommerce/CartItemCard.js +11 -0
- package/lib/components/ecommerce/CheckoutStepper.d.ts +10 -0
- package/lib/components/ecommerce/CheckoutStepper.js +12 -0
- package/lib/components/ecommerce/FloatingCartButton.d.ts +8 -0
- package/lib/components/ecommerce/FloatingCartButton.js +8 -0
- package/lib/components/ecommerce/OrderSummary.d.ts +23 -0
- package/lib/components/ecommerce/OrderSummary.js +9 -0
- package/lib/components/ecommerce/ProductCard.d.ts +21 -0
- package/lib/components/ecommerce/ProductCard.js +62 -0
- package/lib/components/ecommerce/ProductCardSkeleton.d.ts +1 -0
- package/lib/components/ecommerce/ProductCardSkeleton.js +5 -0
- package/lib/components/ecommerce/ProductGrid.d.ts +17 -0
- package/lib/components/ecommerce/ProductGrid.js +14 -0
- package/lib/components/ecommerce/index.d.ts +16 -0
- package/lib/components/ecommerce/index.js +9 -0
- package/lib/components/ecommerce/types.d.ts +113 -0
- package/lib/components/ecommerce/types.js +6 -0
- package/lib/components/index.d.ts +1 -0
- package/lib/components/index.js +1 -0
- package/lib/components/layout/Navbar.d.ts +18 -0
- package/lib/components/layout/Navbar.js +3 -3
- package/lib/components/layout/NavbarActionsDropdown.d.ts +11 -0
- package/lib/components/layout/NavbarActionsDropdown.js +12 -0
- package/lib/components/layout/index.d.ts +1 -0
- package/lib/components/layout/index.js +1 -0
- package/lib/components/sidebar-v2/NavbarV2.js +4 -4
- package/lib/components/ui/AnimatedBackground.d.ts +8 -0
- package/lib/components/ui/AnimatedBackground.js +15 -0
- package/lib/components/ui/Badge.d.ts +7 -0
- package/lib/components/ui/Badge.js +13 -0
- package/lib/components/ui/BarcodeImageDisplay.d.ts +11 -0
- package/lib/components/ui/BarcodeImageDisplay.js +47 -0
- package/lib/components/ui/CardInputForm.d.ts +16 -0
- package/lib/components/ui/CardInputForm.js +112 -0
- package/lib/components/ui/CartDrawer.d.ts +13 -0
- package/lib/components/ui/CartDrawer.js +6 -0
- package/lib/components/ui/Command.d.ts +2 -2
- package/lib/components/ui/CustomAttributeBuilder.d.ts +12 -0
- package/lib/components/ui/CustomAttributeBuilder.js +28 -0
- package/lib/components/ui/DetailsCard.js +1 -3
- package/lib/components/ui/InlineLoginForm.d.ts +10 -0
- package/lib/components/ui/InlineLoginForm.js +33 -0
- package/lib/components/ui/InlineRegisterForm.d.ts +20 -0
- package/lib/components/ui/InlineRegisterForm.js +49 -0
- package/lib/components/ui/ProgressIndicatorButton.d.ts +19 -0
- package/lib/components/ui/ProgressIndicatorButton.js +19 -0
- package/lib/components/ui/RadioPillGroup.d.ts +12 -0
- package/lib/components/ui/RadioPillGroup.js +16 -0
- package/lib/components/ui/Sheet.d.ts +1 -1
- package/lib/components/ui/SidebarHeader.d.ts +9 -0
- package/lib/components/ui/SidebarHeader.js +21 -0
- package/lib/components/ui/StatCard.d.ts +2 -2
- package/lib/components/ui/StepIndicator.d.ts +7 -0
- package/lib/components/ui/StepIndicator.js +5 -0
- package/lib/components/ui/StepProgress.d.ts +13 -0
- package/lib/components/ui/StepProgress.js +18 -0
- package/lib/components/ui/TierConfigBuilder.d.ts +13 -0
- package/lib/components/ui/TierConfigBuilder.js +131 -0
- package/lib/components/ui/index.d.ts +28 -0
- package/lib/components/ui/index.js +27 -0
- package/lib/helpers/validations.d.ts +6 -6
- package/lib/styles.css +475 -0
- package/package.json +1 -1
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Bundle } from './types';
|
|
2
|
+
export interface BundleCardProps {
|
|
3
|
+
bundle: Bundle;
|
|
4
|
+
onAdd: (bundle: Bundle) => void | Promise<void>;
|
|
5
|
+
disabled?: boolean;
|
|
6
|
+
isAdding?: boolean;
|
|
7
|
+
currency?: string;
|
|
8
|
+
/** "button" renders an explicit Add to Cart button. "tap" makes the whole card tappable (POS style). */
|
|
9
|
+
interaction?: 'button' | 'tap';
|
|
10
|
+
}
|
|
11
|
+
export declare function BundleCard({ bundle, onAdd, disabled, isAdding, currency, interaction, }: BundleCardProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { HiOutlineGift, HiOutlineShoppingCart } from 'react-icons/hi';
|
|
3
|
+
import { cn } from '../../helpers/utils';
|
|
4
|
+
import { formatPrice } from '../../helpers/utils';
|
|
5
|
+
import ImageComponent from '../ui/ImageComponent';
|
|
6
|
+
function computeSavings(bundle) {
|
|
7
|
+
if (!bundle.items?.length)
|
|
8
|
+
return { regularTotal: bundle.bundlePrice, savings: 0, savingsPercent: 0 };
|
|
9
|
+
const regularTotal = bundle.items.reduce((sum, item) => {
|
|
10
|
+
const price = Number(item.product?.basePrice ?? 0);
|
|
11
|
+
return sum + price * item.quantity;
|
|
12
|
+
}, 0);
|
|
13
|
+
const savings = regularTotal > bundle.bundlePrice ? regularTotal - bundle.bundlePrice : 0;
|
|
14
|
+
const savingsPercent = regularTotal > 0 ? Math.round((savings / regularTotal) * 100) : 0;
|
|
15
|
+
return { regularTotal, savings, savingsPercent };
|
|
16
|
+
}
|
|
17
|
+
export function BundleCard({ bundle, onAdd, disabled = false, isAdding = false, currency = 'ZAR', interaction = 'button', }) {
|
|
18
|
+
const { regularTotal, savings, savingsPercent } = computeSavings(bundle);
|
|
19
|
+
const hasSavings = savings > 0;
|
|
20
|
+
const isDisabled = !bundle.isActive || disabled || isAdding;
|
|
21
|
+
const isTapMode = interaction === 'tap';
|
|
22
|
+
const handleAdd = () => {
|
|
23
|
+
if (!isDisabled)
|
|
24
|
+
onAdd(bundle);
|
|
25
|
+
};
|
|
26
|
+
const visibleItems = (bundle.items ?? []).slice(0, 3);
|
|
27
|
+
const remainingCount = Math.max(0, (bundle.items?.length ?? 0) - 3);
|
|
28
|
+
return (_jsxs("div", { onClick: isTapMode && !isDisabled ? handleAdd : undefined, onKeyDown: isTapMode && !isDisabled
|
|
29
|
+
? (e) => {
|
|
30
|
+
if (e.key === 'Enter' || e.key === ' ')
|
|
31
|
+
handleAdd();
|
|
32
|
+
}
|
|
33
|
+
: undefined, role: isTapMode ? 'button' : undefined, tabIndex: isTapMode && !isDisabled ? 0 : undefined, "aria-label": isTapMode ? `Add ${bundle.name} bundle to cart` : undefined, className: cn('group relative flex flex-col overflow-hidden rounded-lg bg-background transition-shadow duration-200 sm:rounded-xl', 'border border-border shadow-sm hover:shadow-lg', isDisabled && 'cursor-not-allowed opacity-60', isTapMode && !isDisabled && 'cursor-pointer'), children: [bundle.isActive && hasSavings && (_jsxs("div", { className: "absolute left-2 top-2 z-10 rounded-full bg-success px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-white sm:left-3 sm:top-3 sm:px-2.5 sm:py-1", children: ["Save ", savingsPercent, "%"] })), bundle.isActive && !hasSavings && (_jsx("div", { className: "absolute left-2 top-2 z-10 rounded-full bg-success px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-white sm:left-3 sm:top-3 sm:px-2.5 sm:py-1", children: "Bundle" })), _jsxs("div", { className: "relative aspect-square w-full overflow-hidden bg-muted", children: [bundle.imageUrl ? (_jsx(ImageComponent, { src: bundle.imageUrl, alt: bundle.name, fill: true, className: "object-cover", sizes: "(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 20vw" })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center bg-gradient-to-br from-success/10 to-success/5 text-success/40", children: _jsx(HiOutlineGift, { className: "h-10 w-10 sm:h-12 sm:w-12" }) })), !bundle.isActive && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-black/40", children: _jsx("span", { className: "rounded-full bg-white/90 px-3 py-1 text-[11px] font-bold uppercase tracking-wide text-foreground", children: "Unavailable" }) }))] }), _jsxs("div", { className: "flex flex-1 flex-col p-2 sm:p-4", children: [_jsx("h3", { className: "line-clamp-2 text-xs font-medium leading-snug text-foreground sm:text-sm", children: bundle.name }), visibleItems.length > 0 && (_jsxs("div", { className: "mt-1.5 space-y-0.5", children: [_jsx("p", { className: "text-[10px] font-semibold uppercase tracking-wide text-muted-foreground", children: "Includes:" }), visibleItems.map((item, idx) => (_jsxs("p", { className: "line-clamp-1 text-[11px] text-muted-foreground", children: [item.quantity > 1 ? `${item.quantity}× ` : '', item.product?.name ?? 'Item', item.variant?.name ? ` (${item.variant.name})` : ''] }, item.id || idx))), remainingCount > 0 && _jsxs("p", { className: "text-[11px] font-medium text-primary", children: ["+", remainingCount, " more"] })] })), _jsx("div", { className: "flex-1" }), _jsxs("div", { className: "mt-1.5 sm:mt-3", children: [_jsxs("div", { className: "flex items-baseline gap-2", children: [_jsx("span", { className: "text-sm font-bold text-foreground sm:text-base", children: formatPrice(Number(bundle.bundlePrice) || 0, currency) }), hasSavings && (_jsx("span", { className: "text-[11px] text-muted-foreground line-through", children: formatPrice(regularTotal, currency) }))] }), hasSavings && (_jsxs("p", { className: "mt-0.5 text-[11px] font-medium text-success-text", children: ["Save ", formatPrice(savings, currency)] }))] }), interaction === 'button' && (_jsx("button", { type: "button", disabled: isDisabled, onClick: (e) => {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
e.stopPropagation();
|
|
36
|
+
handleAdd();
|
|
37
|
+
}, className: cn('mt-2 flex h-8 w-full items-center justify-center gap-1.5 rounded-lg text-xs font-medium transition-colors sm:mt-3 sm:h-9 sm:text-sm', !isDisabled
|
|
38
|
+
? 'bg-foreground text-background hover:bg-foreground/90'
|
|
39
|
+
: 'cursor-not-allowed bg-muted text-muted-foreground'), children: isAdding ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "h-3.5 w-3.5 animate-spin rounded-full border-2 border-background/30 border-t-background" }), _jsx("span", { children: "Adding..." })] })) : (_jsxs(_Fragment, { children: [_jsx(HiOutlineShoppingCart, { className: "h-3.5 w-3.5" }), _jsx("span", { children: "Add Bundle" })] })) }))] })] }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EcommerceCartItem } from './types';
|
|
2
|
+
export interface CartItemCardProps {
|
|
3
|
+
item: EcommerceCartItem;
|
|
4
|
+
onUpdateQuantity: (itemId: string, quantity: number) => void;
|
|
5
|
+
onRemove: (itemId: string) => void;
|
|
6
|
+
isUpdating?: boolean;
|
|
7
|
+
isRemoving?: boolean;
|
|
8
|
+
compact?: boolean;
|
|
9
|
+
currency?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function CartItemCard({ item, onUpdateQuantity, onRemove, isUpdating, isRemoving, compact, currency, }: CartItemCardProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HiMinus, HiOutlineGift, HiOutlinePhotograph, HiPlus, HiTrash } from 'react-icons/hi';
|
|
3
|
+
import { cn } from '../../helpers/utils';
|
|
4
|
+
import { formatPrice } from '../../helpers/utils';
|
|
5
|
+
export function CartItemCard({ item, onUpdateQuantity, onRemove, isUpdating, isRemoving, compact = false, currency = 'ZAR', }) {
|
|
6
|
+
const isLoading = isUpdating || isRemoving;
|
|
7
|
+
const imageUrl = item.productImageUrl;
|
|
8
|
+
const isBundle = !!item.metadata?.bundleId;
|
|
9
|
+
const bundleItems = item.metadata?.bundleItems;
|
|
10
|
+
return (_jsxs("div", { className: cn('flex gap-3 border-b border-border py-3 last:border-0 sm:py-4', isLoading && 'pointer-events-none opacity-50', compact ? 'gap-2 sm:gap-3' : 'gap-3 sm:gap-4'), children: [_jsx("div", { className: cn('relative flex-shrink-0 overflow-hidden rounded-lg bg-muted', compact ? 'h-14 w-14 sm:h-16 sm:w-16' : 'h-16 w-16 sm:h-20 sm:w-20'), children: imageUrl ? (_jsx("img", { src: imageUrl, alt: item.productName, className: "h-full w-full object-cover" })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center text-muted-foreground", children: _jsx(HiOutlinePhotograph, { className: cn(compact ? 'h-6 w-6' : 'h-8 w-8') }) })) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex items-start justify-between gap-2", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex items-center gap-1.5", children: [_jsxs("h4", { className: cn('font-medium text-foreground', compact ? 'line-clamp-1 text-xs sm:text-sm' : 'line-clamp-2 text-sm sm:text-base'), children: [item.productName, item.variantName && (_jsxs("span", { className: "ml-1 text-xs font-normal text-muted-foreground", children: ["\u2014 ", item.variantName] }))] }), isBundle && (_jsxs("span", { className: "inline-flex items-center gap-0.5 rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold text-primary", children: [_jsx(HiOutlineGift, { className: "h-3 w-3" }), "Bundle"] }))] }), item.productSku && (_jsxs("p", { className: "mt-0.5 hidden text-xs text-muted-foreground sm:block", children: ["SKU: ", item.productSku] })), isBundle && bundleItems && bundleItems.length > 0 && (_jsx("div", { className: "mt-1 space-y-0.5", children: bundleItems.map((bi) => (_jsxs("p", { className: "text-[10px] text-muted-foreground", children: [bi.quantity, "x ", bi.productName, bi.variantName ? ` (${bi.variantName})` : ''] }, `${bi.productId}-${bi.variantName ?? ''}`))) }))] }), _jsx("button", { type: "button", onClick: () => onRemove(item.id), disabled: isLoading, className: "flex-shrink-0 rounded p-1 text-muted-foreground transition-colors hover:bg-accent hover:text-destructive disabled:opacity-50", children: _jsx(HiTrash, { className: cn(compact ? 'h-4 w-4' : 'h-5 w-5') }) })] }), _jsxs("div", { className: "mt-auto flex items-end justify-between pt-2", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("div", { className: cn('flex items-center gap-1 rounded-full bg-muted', compact ? 'p-0.5' : 'p-1'), children: [_jsx("button", { type: "button", onClick: () => onUpdateQuantity(item.id, item.quantity - 1), disabled: isLoading || item.quantity <= 1, className: cn('flex items-center justify-center rounded-full bg-background shadow-sm transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50', compact ? 'h-6 w-6' : 'h-8 w-8 sm:h-7 sm:w-7'), children: _jsx(HiMinus, { className: "h-3 w-3 text-muted-foreground" }) }), _jsx("span", { className: cn('text-center font-bold text-foreground', compact ? 'w-6 text-xs' : 'w-8 text-sm'), children: item.quantity }), _jsx("button", { type: "button", onClick: () => onUpdateQuantity(item.id, item.quantity + 1), disabled: isLoading, className: cn('flex items-center justify-center rounded-full bg-background shadow-sm transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50', compact ? 'h-6 w-6' : 'h-8 w-8 sm:h-7 sm:w-7'), children: _jsx(HiPlus, { className: "h-3 w-3 text-muted-foreground" }) })] }), _jsxs("span", { className: "text-xs text-muted-foreground", children: ["@ ", formatPrice(item.unitPrice, currency)] })] }), _jsxs("div", { className: "text-right", children: [_jsx("span", { className: cn('font-semibold text-foreground', compact ? 'text-xs sm:text-sm' : 'text-sm sm:text-base'), children: formatPrice(item.lineTotal, currency) }), item.taxAmount > 0 && (_jsxs("p", { className: "text-[10px] text-muted-foreground", children: ["incl. ", formatPrice(item.taxAmount, currency), " VAT"] }))] })] })] })] }));
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface CheckoutStep {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
}
|
|
5
|
+
export interface CheckoutStepperProps {
|
|
6
|
+
steps: CheckoutStep[];
|
|
7
|
+
currentStep: number;
|
|
8
|
+
onStepClick?: (stepIndex: number) => void;
|
|
9
|
+
}
|
|
10
|
+
export declare function CheckoutStepper({ steps, currentStep, onStepClick }: CheckoutStepperProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HiCheck } from 'react-icons/hi';
|
|
3
|
+
import { cn } from '../../helpers/utils';
|
|
4
|
+
export function CheckoutStepper({ steps, currentStep, onStepClick }) {
|
|
5
|
+
return (_jsx("nav", { "aria-label": "Checkout progress", className: "w-full", children: _jsx("ol", { className: "flex items-center justify-between", children: steps.map((step, stepIndex) => {
|
|
6
|
+
const isCompleted = stepIndex < currentStep;
|
|
7
|
+
const isCurrent = stepIndex === currentStep;
|
|
8
|
+
const isClickable = !!onStepClick && stepIndex < currentStep;
|
|
9
|
+
const isLast = stepIndex === steps.length - 1;
|
|
10
|
+
return (_jsxs("li", { className: cn('relative flex flex-1 items-center', isLast && 'flex-none'), children: [_jsxs("div", { className: "flex flex-col items-center", children: [_jsx("button", { type: "button", onClick: () => isClickable && onStepClick?.(stepIndex), disabled: !isClickable, className: cn('relative z-10 flex h-10 w-10 items-center justify-center rounded-full text-sm font-semibold transition-all duration-200', isCompleted && 'bg-foreground text-white', isCurrent && 'border-2 border-foreground bg-background text-foreground', !isCompleted && !isCurrent && 'border-2 border-input bg-background text-muted-foreground', isClickable && 'cursor-pointer hover:bg-foreground/90', !isClickable && 'cursor-default'), "aria-current": isCurrent ? 'step' : undefined, "aria-label": `${isCompleted ? 'Completed: ' : isCurrent ? 'Current: ' : ''}${step.name}`, children: isCompleted ? _jsx(HiCheck, { className: "h-5 w-5", "aria-hidden": "true" }) : _jsx("span", { children: stepIndex + 1 }) }), _jsx("span", { className: cn('mt-3 text-xs font-medium sm:text-sm', isCompleted && 'text-foreground', isCurrent && 'font-semibold text-foreground', !isCompleted && !isCurrent && 'text-muted-foreground'), children: step.name })] }), !isLast && (_jsxs("div", { className: "relative mx-2 h-0.5 flex-1 sm:mx-4", "aria-hidden": "true", children: [_jsx("div", { className: "absolute inset-0 bg-muted" }), _jsx("div", { className: cn('absolute inset-y-0 left-0 bg-foreground transition-all duration-300', isCompleted ? 'w-full' : 'w-0') })] }))] }, step.id));
|
|
11
|
+
}) }) }));
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface FloatingCartButtonProps {
|
|
2
|
+
itemCount: number;
|
|
3
|
+
totalAmount: number;
|
|
4
|
+
onClick: () => void;
|
|
5
|
+
isVisible: boolean;
|
|
6
|
+
currency?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function FloatingCartButton({ itemCount, totalAmount, onClick, isVisible, currency, }: FloatingCartButtonProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HiShoppingCart } from 'react-icons/hi';
|
|
3
|
+
import { formatPrice } from '../../helpers/utils';
|
|
4
|
+
export function FloatingCartButton({ itemCount, totalAmount, onClick, isVisible, currency = 'ZAR', }) {
|
|
5
|
+
if (!isVisible)
|
|
6
|
+
return null;
|
|
7
|
+
return (_jsxs("button", { type: "button", onClick: onClick, "aria-label": `Shopping cart with ${itemCount} items, total ${formatPrice(totalAmount, currency)}`, className: "fixed bottom-4 left-1/2 z-30 flex -translate-x-1/2 items-center gap-2.5 rounded-full bg-foreground px-5 py-3 text-background shadow-xl transition-all hover:shadow-2xl active:scale-95 sm:left-auto sm:right-6 sm:bottom-6 sm:translate-x-0", children: [_jsx(HiShoppingCart, { className: "h-4 w-4" }), _jsx("span", { className: "text-sm font-bold", children: formatPrice(totalAmount, currency) }), _jsx("span", { className: "flex h-5 w-5 items-center justify-center rounded-full bg-background text-[10px] font-bold text-foreground", children: itemCount })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EcommerceCartItem } from './types';
|
|
2
|
+
export interface OrderSummaryProps {
|
|
3
|
+
items?: EcommerceCartItem[];
|
|
4
|
+
subtotal: number;
|
|
5
|
+
taxAmount: number;
|
|
6
|
+
totalAmount: number;
|
|
7
|
+
discountAmount?: number;
|
|
8
|
+
showItems?: boolean;
|
|
9
|
+
showCheckoutButton?: boolean;
|
|
10
|
+
onCheckout?: () => void;
|
|
11
|
+
isLoading?: boolean;
|
|
12
|
+
checkoutButtonText?: string;
|
|
13
|
+
priceDisplayMode?: 'inclusive' | 'exclusive';
|
|
14
|
+
currency?: string;
|
|
15
|
+
securityText?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function OrderSummary({ items, subtotal, taxAmount, totalAmount, discountAmount, showItems, showCheckoutButton, onCheckout, isLoading, checkoutButtonText, priceDisplayMode, currency, securityText, }: OrderSummaryProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
export interface OrderSummaryCompactProps {
|
|
19
|
+
itemCount: number;
|
|
20
|
+
totalAmount: number;
|
|
21
|
+
currency?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function OrderSummaryCompact({ itemCount, totalAmount, currency }: OrderSummaryCompactProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { HiOutlineLockClosed } from 'react-icons/hi';
|
|
3
|
+
import { formatPrice } from '../../helpers/utils';
|
|
4
|
+
export function OrderSummary({ items = [], subtotal, taxAmount, totalAmount, discountAmount = 0, showItems = false, showCheckoutButton = false, onCheckout, isLoading, checkoutButtonText = 'Proceed to Checkout', priceDisplayMode = 'exclusive', currency = 'ZAR', securityText, }) {
|
|
5
|
+
return (_jsxs("div", { className: "rounded-xl border border-border bg-background p-3 sm:p-5", children: [_jsx("h3", { className: "mb-2 text-sm font-bold text-foreground sm:mb-3 sm:text-base", children: "Order Summary" }), showItems && items.length > 0 && (_jsx("div", { className: "mb-4 max-h-[300px] space-y-3 overflow-y-auto border-b border-border pb-4", children: items.map((item) => (_jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsxs("span", { className: "text-muted-foreground", children: [item.quantity, "x"] }), _jsx("div", { className: "min-w-0", children: _jsxs("span", { className: "line-clamp-1 text-foreground", children: [item.productName, item.variantName && (_jsxs("span", { className: "ml-1 text-xs text-muted-foreground", children: ["\u2014 ", item.variantName] }))] }) })] }), _jsxs("div", { className: "text-right", children: [_jsx("span", { className: "font-medium text-foreground", children: formatPrice(item.lineTotal, currency) }), item.taxAmount > 0 && (_jsxs("p", { className: "text-[10px] text-muted-foreground", children: ["incl. ", formatPrice(item.taxAmount, currency), " VAT"] }))] })] }, item.id))) })), _jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsxs("span", { className: "text-muted-foreground", children: ["Subtotal ", _jsx("span", { className: "text-xs", children: "(excl. VAT)" })] }), _jsx("span", { className: "font-medium text-foreground", children: formatPrice(subtotal, currency) })] }), _jsxs("div", { className: "flex items-center justify-between text-sm", children: [_jsxs("span", { className: "text-muted-foreground", children: ["VAT ", _jsxs("span", { className: "text-xs", children: ["(", priceDisplayMode === 'inclusive' ? 'included' : 'added', ")"] })] }), _jsx("span", { className: "font-medium text-foreground", children: formatPrice(taxAmount, currency) })] }), discountAmount > 0 && (_jsxs("div", { className: "flex items-center justify-between text-sm text-success", children: [_jsx("span", { children: "Discount" }), _jsxs("span", { className: "font-medium", children: ["-", formatPrice(discountAmount, currency)] })] })), _jsx("div", { className: "border-t border-border pt-3", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-lg font-semibold text-foreground", children: "Total" }), _jsx("span", { className: "text-lg font-bold text-foreground sm:text-xl", children: formatPrice(totalAmount, currency) })] }) })] }), showCheckoutButton && (_jsxs("div", { className: "mt-6 space-y-3", children: [_jsx("button", { type: "button", className: "flex h-11 w-full items-center justify-center rounded-lg bg-foreground text-sm font-semibold text-background transition-colors hover:bg-foreground/90 disabled:cursor-not-allowed disabled:opacity-50", onClick: onCheckout, disabled: isLoading || items.length === 0, children: isLoading ? ('Processing...') : (_jsxs(_Fragment, { children: [_jsx(HiOutlineLockClosed, { className: "mr-2 h-4 w-4" }), checkoutButtonText] })) }), securityText && _jsx("p", { className: "text-center text-xs text-muted-foreground", children: securityText })] }))] }));
|
|
6
|
+
}
|
|
7
|
+
export function OrderSummaryCompact({ itemCount, totalAmount, currency = 'ZAR' }) {
|
|
8
|
+
return (_jsxs("div", { className: "flex items-center justify-between rounded-lg bg-muted px-4 py-3", children: [_jsx("div", { children: _jsxs("span", { className: "text-sm text-muted-foreground", children: [itemCount, " item", itemCount !== 1 ? 's' : ''] }) }), _jsx("div", { className: "text-right", children: _jsx("span", { className: "text-lg font-bold text-foreground", children: formatPrice(totalAmount, currency) }) })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EcommerceProduct } from './types';
|
|
2
|
+
export interface ProductCardProps {
|
|
3
|
+
product: EcommerceProduct;
|
|
4
|
+
onAddToCart?: (product: EcommerceProduct) => void;
|
|
5
|
+
isAddingToCart?: boolean;
|
|
6
|
+
storeId?: string;
|
|
7
|
+
basePath?: string;
|
|
8
|
+
featured?: boolean;
|
|
9
|
+
linkSuffix?: string;
|
|
10
|
+
currency?: string;
|
|
11
|
+
/** Render function for the product link wrapper */
|
|
12
|
+
renderLink?: (props: {
|
|
13
|
+
to: string;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
tabIndex?: number;
|
|
17
|
+
'aria-label'?: string;
|
|
18
|
+
}) => React.ReactNode;
|
|
19
|
+
onNavigate?: (productUrl: string) => void;
|
|
20
|
+
}
|
|
21
|
+
export declare function ProductCard({ product, onAddToCart, isAddingToCart, storeId, basePath, featured, linkSuffix, currency, renderLink, onNavigate, }: ProductCardProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { HiOutlineShoppingCart, HiOutlineViewGrid } from 'react-icons/hi';
|
|
3
|
+
import { cn } from '../../helpers/utils';
|
|
4
|
+
import { formatPrice } from '../../helpers/utils';
|
|
5
|
+
import ImageComponent from '../ui/ImageComponent';
|
|
6
|
+
const LOW_STOCK_THRESHOLD = 5;
|
|
7
|
+
export function ProductCard({ product, onAddToCart, isAddingToCart, storeId, basePath = '/shop', featured = false, linkSuffix, currency = 'ZAR', renderLink, onNavigate, }) {
|
|
8
|
+
const hasStock = product.totalStock > 0;
|
|
9
|
+
const isLowStock = hasStock && product.totalStock <= LOW_STOCK_THRESHOLD;
|
|
10
|
+
const isBackorderable = product.allowBackorders === true;
|
|
11
|
+
const canAddToCart = (hasStock || isBackorderable) && !!storeId;
|
|
12
|
+
const hasVariants = product.variants && product.variants.length > 0;
|
|
13
|
+
const priceDisplay = (() => {
|
|
14
|
+
if (!hasVariants) {
|
|
15
|
+
return formatPrice(product.displayPrice ?? product.basePrice, currency);
|
|
16
|
+
}
|
|
17
|
+
const prices = product.variants.map((v) => v.displayPrice ?? v.basePrice);
|
|
18
|
+
const basePrice = product.displayPrice ?? product.basePrice;
|
|
19
|
+
const allPrices = [basePrice, ...prices];
|
|
20
|
+
const min = Math.min(...allPrices);
|
|
21
|
+
const max = Math.max(...allPrices);
|
|
22
|
+
if (min === max)
|
|
23
|
+
return formatPrice(min, currency);
|
|
24
|
+
return `${formatPrice(min, currency)} – ${formatPrice(max, currency)}`;
|
|
25
|
+
})();
|
|
26
|
+
const productUrl = `${basePath}/${product.id}${linkSuffix ?? ''}`;
|
|
27
|
+
const LinkWrapper = ({ children, className, tabIndex, ariaLabel, }) => {
|
|
28
|
+
if (renderLink) {
|
|
29
|
+
return (_jsx(_Fragment, { children: renderLink({
|
|
30
|
+
to: productUrl,
|
|
31
|
+
children,
|
|
32
|
+
className,
|
|
33
|
+
tabIndex,
|
|
34
|
+
'aria-label': ariaLabel,
|
|
35
|
+
}) }));
|
|
36
|
+
}
|
|
37
|
+
return (_jsx("a", { href: productUrl, className: className, tabIndex: tabIndex, "aria-label": ariaLabel, children: children }));
|
|
38
|
+
};
|
|
39
|
+
return (_jsxs("div", { className: cn('group relative flex flex-col overflow-hidden bg-background transition-shadow duration-200', 'rounded-lg sm:rounded-xl', 'hover:shadow-lg', featured ? 'border-2 border-foreground shadow-md' : 'border border-border shadow-sm'), children: [_jsxs(LinkWrapper, { className: "relative block aspect-square w-full overflow-hidden bg-muted", tabIndex: -1, ariaLabel: `View ${product.name}`, children: [product.imageUrl ? (_jsx(ImageComponent, { src: product.imageUrl, alt: product.name, fill: true, className: "object-cover", sizes: "(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw" })) : (_jsx("div", { className: "flex h-full w-full items-center justify-center bg-muted text-muted-foreground", children: _jsx("svg", { className: "h-10 w-10 sm:h-12 sm:w-12", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 1, d: "M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" }) }) })), !hasStock && !isBackorderable && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-black/40", children: _jsx("span", { className: "rounded-full bg-white/90 px-3 py-1 text-[11px] font-bold uppercase tracking-wide text-foreground sm:px-4 sm:py-1.5 sm:text-xs", children: "Out of Stock" }) })), !hasStock && isBackorderable && (_jsx("div", { className: "absolute left-2 top-2 z-10 rounded-full bg-warning px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-white sm:left-3 sm:top-3 sm:px-2.5 sm:py-1", children: "Backorder" })), featured && hasStock && (_jsx("div", { className: "absolute left-2 top-2 z-10 rounded-full bg-foreground px-2 py-0.5 text-[10px] font-bold uppercase tracking-wide text-white sm:left-3 sm:top-3 sm:px-2.5 sm:py-1", children: "Popular" }))] }), _jsxs("div", { className: "flex flex-1 flex-col p-2 sm:p-4", children: [product.category && (_jsx("span", { className: "mb-0.5 hidden text-[10px] font-medium uppercase tracking-wide text-muted-foreground sm:block", children: product.category.name })), _jsx(LinkWrapper, { children: _jsx("h3", { className: "line-clamp-2 text-xs font-medium leading-snug text-foreground transition-colors hover:text-primary sm:text-sm", children: product.name }) }), _jsx("div", { className: "flex-1" }), _jsxs("div", { className: "mt-1.5 sm:mt-3", children: [_jsxs("div", { className: "flex items-baseline justify-between gap-1", children: [_jsx("span", { className: "text-sm font-bold text-foreground sm:text-base", children: priceDisplay }), hasVariants && (_jsxs("span", { className: "hidden text-[11px] font-medium text-muted-foreground sm:inline", children: [product.variants.length, " option", product.variants.length !== 1 ? 's' : ''] }))] }), isLowStock && _jsxs("p", { className: "mt-0.5 text-[11px] font-medium text-warning", children: ["Only ", product.totalStock, " left"] }), hasStock && !isLowStock && (_jsxs("p", { className: "mt-0.5 hidden text-[11px] text-muted-foreground sm:block", children: [product.totalStock, " in stock"] }))] }), _jsx("button", { type: "button", disabled: hasVariants ? !hasStock && !isBackorderable : !canAddToCart || isAddingToCart, onClick: (e) => {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
e.stopPropagation();
|
|
42
|
+
if (hasVariants) {
|
|
43
|
+
if (onNavigate) {
|
|
44
|
+
onNavigate(productUrl);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
window.location.href = productUrl;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (onAddToCart && canAddToCart) {
|
|
51
|
+
onAddToCart(product);
|
|
52
|
+
}
|
|
53
|
+
}, className: cn('mt-2 flex h-8 w-full items-center justify-center gap-1.5 rounded-lg text-xs font-medium transition-colors sm:mt-3 sm:h-9 sm:text-sm', hasVariants
|
|
54
|
+
? hasStock || isBackorderable
|
|
55
|
+
? 'bg-foreground text-background hover:bg-foreground/90 active:bg-foreground/80'
|
|
56
|
+
: 'cursor-not-allowed bg-muted text-muted-foreground'
|
|
57
|
+
: canAddToCart && !isAddingToCart
|
|
58
|
+
? isBackorderable && !hasStock
|
|
59
|
+
? 'bg-warning text-white hover:bg-warning/90'
|
|
60
|
+
: 'bg-foreground text-background hover:bg-foreground/90 active:bg-foreground/80'
|
|
61
|
+
: 'cursor-not-allowed bg-muted text-muted-foreground'), children: hasVariants ? (_jsxs(_Fragment, { children: [_jsx(HiOutlineViewGrid, { className: "h-3.5 w-3.5" }), _jsx("span", { children: !hasStock && !isBackorderable ? 'Out of Stock' : 'Options' })] })) : isAddingToCart ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "h-3.5 w-3.5 animate-spin rounded-full border-2 border-background/30 border-t-background" }), _jsx("span", { children: "Adding..." })] })) : (_jsxs(_Fragment, { children: [_jsx(HiOutlineShoppingCart, { className: "h-3.5 w-3.5" }), _jsx("span", { children: !storeId ? 'Select Store' : hasStock ? 'Add to Cart' : isBackorderable ? 'Backorder' : 'Sold Out' })] })) })] })] }));
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function ProductCardSkeleton(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Skeleton } from '../ui/Skeleton';
|
|
3
|
+
export function ProductCardSkeleton() {
|
|
4
|
+
return (_jsxs("div", { className: "overflow-hidden rounded-lg border border-border bg-background sm:rounded-xl", children: [_jsx(Skeleton, { className: "aspect-square w-full rounded-none" }), _jsxs("div", { className: "space-y-2 p-2 sm:p-4", children: [_jsx(Skeleton, { className: "h-3 w-3/4" }), _jsx(Skeleton, { className: "h-3 w-1/2" }), _jsx("div", { className: "pt-1", children: _jsx(Skeleton, { className: "h-4 w-20" }) }), _jsx(Skeleton, { className: "h-8 w-full rounded-lg sm:h-9" })] })] }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ProductCard } from './ProductCard';
|
|
2
|
+
import type { EcommerceProduct } from './types';
|
|
3
|
+
export interface ProductGridProps {
|
|
4
|
+
products: EcommerceProduct[];
|
|
5
|
+
isLoading?: boolean;
|
|
6
|
+
onAddToCart?: (product: EcommerceProduct) => void;
|
|
7
|
+
addingProductId?: string;
|
|
8
|
+
storeId?: string;
|
|
9
|
+
emptyMessage?: string;
|
|
10
|
+
basePath?: string;
|
|
11
|
+
linkSuffix?: string;
|
|
12
|
+
skeletonCount?: number;
|
|
13
|
+
currency?: string;
|
|
14
|
+
renderLink?: React.ComponentProps<typeof ProductCard>['renderLink'];
|
|
15
|
+
onNavigate?: (url: string) => void;
|
|
16
|
+
}
|
|
17
|
+
export declare function ProductGrid({ products, isLoading, onAddToCart, addingProductId, storeId, emptyMessage, basePath, linkSuffix, skeletonCount, currency, renderLink, onNavigate, }: ProductGridProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import EmptyState from '../ui/EmptyState';
|
|
3
|
+
import { ProductCard } from './ProductCard';
|
|
4
|
+
import { ProductCardSkeleton } from './ProductCardSkeleton';
|
|
5
|
+
const GRID_CLASSES = 'grid grid-cols-2 gap-2 sm:gap-4 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5';
|
|
6
|
+
export function ProductGrid({ products, isLoading, onAddToCart, addingProductId, storeId, emptyMessage = 'No products found', basePath, linkSuffix, skeletonCount = 8, currency, renderLink, onNavigate, }) {
|
|
7
|
+
if (isLoading) {
|
|
8
|
+
return (_jsx("div", { className: GRID_CLASSES, children: Array.from({ length: skeletonCount }, (_, i) => (_jsx(ProductCardSkeleton, {}, i))) }));
|
|
9
|
+
}
|
|
10
|
+
if (products.length === 0) {
|
|
11
|
+
return (_jsx("div", { className: "flex min-h-[200px] items-center justify-center sm:min-h-[300px]", children: _jsx(EmptyState, { title: emptyMessage }) }));
|
|
12
|
+
}
|
|
13
|
+
return (_jsx("div", { className: GRID_CLASSES, children: products.map((product) => (_jsx(ProductCard, { product: product, onAddToCart: onAddToCart, isAddingToCart: addingProductId === product.id, storeId: storeId, basePath: basePath, linkSuffix: linkSuffix, currency: currency, renderLink: renderLink, onNavigate: onNavigate }, product.id))) }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { ProductCard } from './ProductCard';
|
|
2
|
+
export type { ProductCardProps } from './ProductCard';
|
|
3
|
+
export { ProductCardSkeleton } from './ProductCardSkeleton';
|
|
4
|
+
export { ProductGrid } from './ProductGrid';
|
|
5
|
+
export type { ProductGridProps } from './ProductGrid';
|
|
6
|
+
export { CartItemCard } from './CartItemCard';
|
|
7
|
+
export type { CartItemCardProps } from './CartItemCard';
|
|
8
|
+
export { OrderSummary, OrderSummaryCompact } from './OrderSummary';
|
|
9
|
+
export type { OrderSummaryProps, OrderSummaryCompactProps } from './OrderSummary';
|
|
10
|
+
export { FloatingCartButton } from './FloatingCartButton';
|
|
11
|
+
export type { FloatingCartButtonProps } from './FloatingCartButton';
|
|
12
|
+
export { CheckoutStepper } from './CheckoutStepper';
|
|
13
|
+
export type { CheckoutStepperProps, CheckoutStep } from './CheckoutStepper';
|
|
14
|
+
export { BundleCard } from './BundleCard';
|
|
15
|
+
export type { BundleCardProps } from './BundleCard';
|
|
16
|
+
export type { EcommerceCategory, EcommerceProduct, ProductVariant, EcommerceCartItem, CartItemBundleItem, CartItemMetadata, Bundle, BundleItem, } from './types';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Ecommerce Components
|
|
2
|
+
export { ProductCard } from './ProductCard';
|
|
3
|
+
export { ProductCardSkeleton } from './ProductCardSkeleton';
|
|
4
|
+
export { ProductGrid } from './ProductGrid';
|
|
5
|
+
export { CartItemCard } from './CartItemCard';
|
|
6
|
+
export { OrderSummary, OrderSummaryCompact } from './OrderSummary';
|
|
7
|
+
export { FloatingCartButton } from './FloatingCartButton';
|
|
8
|
+
export { CheckoutStepper } from './CheckoutStepper';
|
|
9
|
+
export { BundleCard } from './BundleCard';
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ecommerce Types
|
|
3
|
+
*
|
|
4
|
+
* Shared type definitions for ecommerce UI components.
|
|
5
|
+
*/
|
|
6
|
+
export interface EcommerceCategory {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
imageUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ProductVariant {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
sku?: string | null;
|
|
16
|
+
basePrice: number;
|
|
17
|
+
displayPrice?: number;
|
|
18
|
+
taxRate?: number;
|
|
19
|
+
imageUrl?: string;
|
|
20
|
+
attributes?: Record<string, string>;
|
|
21
|
+
quantityAvailable?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface EcommerceProduct {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
sku: string;
|
|
27
|
+
description?: string;
|
|
28
|
+
basePrice: number;
|
|
29
|
+
displayPrice?: number;
|
|
30
|
+
imageUrl?: string;
|
|
31
|
+
category?: EcommerceCategory;
|
|
32
|
+
availableStores: number;
|
|
33
|
+
totalStock: number;
|
|
34
|
+
inStock?: boolean;
|
|
35
|
+
allowBackorders?: boolean;
|
|
36
|
+
variants?: ProductVariant[];
|
|
37
|
+
}
|
|
38
|
+
export interface CartItemBundleItem {
|
|
39
|
+
productId: string;
|
|
40
|
+
productName: string;
|
|
41
|
+
variantName?: string;
|
|
42
|
+
quantity: number;
|
|
43
|
+
unitPrice: number;
|
|
44
|
+
}
|
|
45
|
+
export interface CartItemMetadata {
|
|
46
|
+
bundleId?: string;
|
|
47
|
+
bundleName?: string;
|
|
48
|
+
bundleItems?: CartItemBundleItem[];
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
}
|
|
51
|
+
export interface EcommerceCartItem {
|
|
52
|
+
id: string;
|
|
53
|
+
cartId: string;
|
|
54
|
+
productId: string;
|
|
55
|
+
variantId?: string;
|
|
56
|
+
variantName?: string;
|
|
57
|
+
productName: string;
|
|
58
|
+
productSku?: string;
|
|
59
|
+
productImageUrl?: string;
|
|
60
|
+
quantity: number;
|
|
61
|
+
unitPrice: number;
|
|
62
|
+
subtotal: number;
|
|
63
|
+
taxAmount: number;
|
|
64
|
+
discountAmount: number;
|
|
65
|
+
totalAmount: number;
|
|
66
|
+
lineTotal: number;
|
|
67
|
+
metadata?: CartItemMetadata | null;
|
|
68
|
+
}
|
|
69
|
+
export interface BundleItem {
|
|
70
|
+
id: string;
|
|
71
|
+
bundleId: string;
|
|
72
|
+
productId: string;
|
|
73
|
+
variantId?: string | null;
|
|
74
|
+
quantity: number;
|
|
75
|
+
sortOrder?: number;
|
|
76
|
+
createdAt?: string;
|
|
77
|
+
updatedAt?: string;
|
|
78
|
+
product?: {
|
|
79
|
+
id: string;
|
|
80
|
+
name: string;
|
|
81
|
+
sku?: string;
|
|
82
|
+
basePrice?: number | null;
|
|
83
|
+
displayPrice?: number | null;
|
|
84
|
+
imageUrl?: string;
|
|
85
|
+
variants?: Array<{
|
|
86
|
+
id: string;
|
|
87
|
+
name: string;
|
|
88
|
+
sku?: string | null;
|
|
89
|
+
basePrice?: number | null;
|
|
90
|
+
}>;
|
|
91
|
+
};
|
|
92
|
+
variant?: {
|
|
93
|
+
id: string;
|
|
94
|
+
name: string;
|
|
95
|
+
sku?: string;
|
|
96
|
+
basePrice?: number;
|
|
97
|
+
} | null;
|
|
98
|
+
}
|
|
99
|
+
export interface Bundle {
|
|
100
|
+
id: string;
|
|
101
|
+
organizationId: string;
|
|
102
|
+
sku?: string;
|
|
103
|
+
name: string;
|
|
104
|
+
description?: string | null;
|
|
105
|
+
bundlePrice: number;
|
|
106
|
+
imageUrl?: string | null;
|
|
107
|
+
contextType?: string | null;
|
|
108
|
+
contextId?: string | null;
|
|
109
|
+
isActive: boolean;
|
|
110
|
+
createdAt?: string;
|
|
111
|
+
updatedAt?: string;
|
|
112
|
+
items?: BundleItem[];
|
|
113
|
+
}
|
package/lib/components/index.js
CHANGED
|
@@ -59,6 +59,24 @@ interface AppDashboardNavbarProps extends AppUserDropdownProps {
|
|
|
59
59
|
* Custom locale names to display in the locale switcher
|
|
60
60
|
*/
|
|
61
61
|
localeNames?: Record<string, string>;
|
|
62
|
+
/**
|
|
63
|
+
* Custom render function for the user dropdown area.
|
|
64
|
+
* When provided, replaces the default AppUserDropdown entirely.
|
|
65
|
+
* Use this for guest auth states, custom login/register buttons, etc.
|
|
66
|
+
*/
|
|
67
|
+
renderUserDropdown?: () => React.ReactNode;
|
|
68
|
+
/**
|
|
69
|
+
* User info to display in the default user dropdown.
|
|
70
|
+
* Only used when renderUserDropdown is NOT provided.
|
|
71
|
+
*/
|
|
72
|
+
userInfo?: {
|
|
73
|
+
name?: string;
|
|
74
|
+
email?: string;
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Custom inline styles applied to the root navbar element.
|
|
78
|
+
*/
|
|
79
|
+
style?: React.CSSProperties;
|
|
62
80
|
/**
|
|
63
81
|
* Configuration for the environment banner
|
|
64
82
|
*/
|
|
@@ -10,7 +10,7 @@ import { LocaleSwitcher, useLibTranslations, useTranslation } from '../../transl
|
|
|
10
10
|
import Button from '../ui/Button';
|
|
11
11
|
const AppSidebarToggleButton = React.memo(({ isMobileOpen, onToggle }) => {
|
|
12
12
|
const { tLib } = useLibTranslations();
|
|
13
|
-
return (_jsxs(Button, { type: "button", variant: "ghost", onClick: onToggle, className: "mr-
|
|
13
|
+
return (_jsxs(Button, { type: "button", variant: "ghost", onClick: onToggle, className: "mr-2 cursor-pointer rounded p-1.5 text-foreground/70 hover:bg-accent hover:text-foreground sm:mr-3 sm:p-2", children: [_jsx("span", { className: "sr-only", children: tLib('sidebar.toggle', 'Toggle sidebar') }), _jsx("div", { className: "md:hidden", children: isMobileOpen ? _jsx(HiX, { className: "h-5 w-5 sm:h-6 sm:w-6" }) : _jsx(HiMenuAlt1, { className: "h-5 w-5 sm:h-6 sm:w-6" }) }), _jsx("div", { className: "hidden md:block", children: _jsx(HiMenuAlt1, { className: "h-5 w-5 sm:h-6 sm:w-6" }) })] }));
|
|
14
14
|
});
|
|
15
15
|
const AppDarkThemeToggleWithTooltip = React.memo(() => {
|
|
16
16
|
const { tLib } = useLibTranslations();
|
|
@@ -48,7 +48,7 @@ const EnvironmentBanner = ({ show = false, text, bgColor = 'red', color = 'black
|
|
|
48
48
|
letterSpacing: '0.1em',
|
|
49
49
|
}, children: text }) }) }));
|
|
50
50
|
};
|
|
51
|
-
const AppDashboardNavbar = ({ dashboardLink = '/dashboard', brandImage, linkComponent, showThemeToggler = true, appLabel, onSelectChangePassword, onSelectProfile, onLogout, hideSidebarToggle = false, navBarActionComponents = [], showLocaleSwitcher = false, localeNames, environmentBanner = { show: false, text: '' }, navbarBackground = { light: 'bg-purple', dark: '' }, navbarTextColors = { brand: 'text-white', general: 'text-white' }, }) => {
|
|
51
|
+
const AppDashboardNavbar = ({ dashboardLink = '/dashboard', brandImage, linkComponent, showThemeToggler = true, appLabel, onSelectChangePassword, onSelectProfile, onLogout, hideSidebarToggle = false, navBarActionComponents = [], showLocaleSwitcher = false, localeNames, renderUserDropdown, userInfo, style, environmentBanner = { show: false, text: '' }, navbarBackground = { light: 'bg-purple', dark: '' }, navbarTextColors = { brand: 'text-white', general: 'text-white' }, }) => {
|
|
52
52
|
const sidebar = useAppSidebarContext();
|
|
53
53
|
const { isMd } = useMediaQueries();
|
|
54
54
|
const { t } = useTranslation();
|
|
@@ -61,7 +61,7 @@ const AppDashboardNavbar = ({ dashboardLink = '/dashboard', brandImage, linkComp
|
|
|
61
61
|
}
|
|
62
62
|
}, [isMd, sidebar.desktop, sidebar.mobile]);
|
|
63
63
|
const navbarClasses = `fixed top-0 w-full border-b border-border ${navbarBackground.light} p-0 ${navbarBackground.dark} sm:p-0`;
|
|
64
|
-
return (_jsxs(_Fragment, { children: [_jsx(Navbar, { fluid: true, className: navbarClasses, style: { zIndex: 100 }, children: _jsx("div", { className: "w-full p-3 pr-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center", children: [!hideSidebarToggle && (_jsx(AppSidebarToggleButton, { isMobileOpen: sidebar.mobile.isOpen, onToggle: handleToggleSidebar })), _jsxs(NavbarBrand, { as: linkComponent, href: dashboardLink, className: "mr-14", children: [brandImage && (_jsx("img", { src: brandImage.src, alt: t(brandImage.alt), width: brandImage.width, height: brandImage.height, className: "h-8" })), _jsx("p", { className: `font-bold ${navbarTextColors.brand} text-[15px] mb-0`, children: t(appLabel) })] })] }), _jsxs("div", { className: "flex items-center lg:gap-3", children: [navBarActionComponents.map((action) => (_jsx(React.Fragment, { children: action }, `app-nav-bar-component-${uuidv4()}`))), showThemeToggler && _jsx(AppDarkThemeToggleWithTooltip, {}), showLocaleSwitcher && (_jsx("div", { className: `mx-2 ${navbarTextColors.general}`, children: _jsx(LocaleSwitcher, { localeNames: localeNames, className: "min-w-[120px] text-sm text-white bg-background z-50" }) })), _jsx("div", { className: "flex items-center", children: _jsx(AppUserDropdown, { onLogout: onLogout, onSelectChangePassword: onSelectChangePassword, onSelectProfile: onSelectProfile, navbarTextColors: navbarTextColors }) })] })] }) }) }), _jsx(EnvironmentBanner, { ...environmentBanner })] }));
|
|
64
|
+
return (_jsxs(_Fragment, { children: [_jsx(Navbar, { fluid: true, className: navbarClasses, style: { zIndex: 100, ...style }, children: _jsx("div", { className: "w-full p-2 pr-3 sm:p-3 sm:pr-4", children: _jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { className: "flex items-center", children: [!hideSidebarToggle && (_jsx(AppSidebarToggleButton, { isMobileOpen: sidebar.mobile.isOpen, onToggle: handleToggleSidebar })), _jsxs(NavbarBrand, { as: linkComponent, href: dashboardLink, className: "mr-4 sm:mr-8 lg:mr-14", children: [brandImage && (_jsx("img", { src: brandImage.src, alt: t(brandImage.alt), width: brandImage.width, height: brandImage.height, className: "h-6 sm:h-8" })), _jsx("p", { className: `font-bold ${navbarTextColors.brand} text-xs sm:text-[15px] mb-0`, children: t(appLabel) })] })] }), _jsxs("div", { className: "flex items-center gap-1 sm:gap-2 lg:gap-3", children: [navBarActionComponents.map((action) => (_jsx(React.Fragment, { children: action }, `app-nav-bar-component-${uuidv4()}`))), showThemeToggler && _jsx(AppDarkThemeToggleWithTooltip, {}), showLocaleSwitcher && (_jsx("div", { className: `mx-2 ${navbarTextColors.general}`, children: _jsx(LocaleSwitcher, { localeNames: localeNames, className: "min-w-[120px] text-sm text-white bg-background z-50" }) })), _jsx("div", { className: "flex items-center", children: renderUserDropdown ? (renderUserDropdown()) : (_jsx(AppUserDropdown, { onLogout: onLogout, onSelectChangePassword: onSelectChangePassword, onSelectProfile: onSelectProfile, navbarTextColors: navbarTextColors })) })] })] }) }) }), _jsx(EnvironmentBanner, { ...environmentBanner })] }));
|
|
65
65
|
};
|
|
66
66
|
export default AppDashboardNavbar;
|
|
67
67
|
export { AppSidebarToggleButton, AppDarkThemeToggleWithTooltip, AppUserDropdown, AppDashboardNavbar };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const NavbarActionsDropdown: ({ children }: {
|
|
2
|
+
children: React.ReactNode;
|
|
3
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
4
|
+
export declare const NavbarActionItem: ({ icon: Icon, label, onClick, badge, }: {
|
|
5
|
+
icon: React.ComponentType<{
|
|
6
|
+
className?: string;
|
|
7
|
+
}>;
|
|
8
|
+
label: string;
|
|
9
|
+
onClick?: () => void;
|
|
10
|
+
badge?: number;
|
|
11
|
+
}) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { IconDots } from '@tabler/icons-react';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import Button from '../ui/Button';
|
|
5
|
+
import { Popover, PopoverContent, PopoverTrigger } from '../ui/Popover';
|
|
6
|
+
export const NavbarActionsDropdown = ({ children }) => {
|
|
7
|
+
const [open, setOpen] = useState(false);
|
|
8
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "hidden md:flex md:items-center md:gap-2", children: children }), _jsx("div", { className: "flex md:hidden", children: _jsxs(Popover, { open: open, onOpenChange: setOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { variant: "outline", size: "sm", className: "h-9 w-9 border-border p-0 hover:bg-accent", "aria-label": "More actions", children: _jsx(IconDots, { className: "h-5 w-5" }) }) }), _jsx(PopoverContent, { className: "w-48 p-2", align: "end", children: _jsx("div", { className: "flex flex-col gap-1", children: children }) })] }) })] }));
|
|
9
|
+
};
|
|
10
|
+
export const NavbarActionItem = ({ icon: Icon, label, onClick, badge, }) => {
|
|
11
|
+
return (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "ghost", size: "icon", onClick: onClick, className: "relative hidden h-9 w-9 items-center justify-center rounded-md border border-border bg-background text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-foreground md:flex", title: label, children: [_jsx(Icon, { className: "h-5 w-5 stroke-[1.5]" }), badge !== undefined && badge > 0 && (_jsx("span", { className: "absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-destructive text-[10px] font-bold text-white", children: badge }))] }), _jsxs(Button, { variant: "ghost", onClick: onClick, className: "flex w-full items-center gap-3 rounded-md px-3 py-2 text-left text-sm transition-colors hover:bg-accent md:hidden", children: [_jsx(Icon, { className: "h-4 w-4 stroke-[1.5] text-muted-foreground" }), _jsx("span", { className: "flex-1 text-foreground", children: label }), badge !== undefined && badge > 0 && (_jsx("span", { className: "flex h-5 min-w-[20px] items-center justify-center rounded-full bg-destructive px-1.5 text-[10px] font-bold text-white", children: badge }))] })] }));
|
|
12
|
+
};
|
|
@@ -6,3 +6,4 @@ export { default as AppPageHeader, type AppPageHeaderProps } from './PageHeader'
|
|
|
6
6
|
export { default as AppDashboardSidebar, AppDesktopSidebar, AppMobileSidebar, AppSidebarMenu } from './Sidebar';
|
|
7
7
|
export { default as AppLayout, type AppLayoutProps } from './AppLayout';
|
|
8
8
|
export { default as SessionExpiryModal, type SessionExpiryModalProps } from './SessionExpiryModal';
|
|
9
|
+
export { NavbarActionsDropdown, NavbarActionItem } from './NavbarActionsDropdown';
|
|
@@ -6,3 +6,4 @@ export { default as AppPageHeader } from './PageHeader';
|
|
|
6
6
|
export { default as AppDashboardSidebar, AppDesktopSidebar, AppMobileSidebar, AppSidebarMenu } from './Sidebar';
|
|
7
7
|
export { default as AppLayout } from './AppLayout';
|
|
8
8
|
export { default as SessionExpiryModal } from './SessionExpiryModal';
|
|
9
|
+
export { NavbarActionsDropdown, NavbarActionItem } from './NavbarActionsDropdown';
|