@deneb-ui/ui 2.0.126 → 2.0.128
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/README.md +9 -1
- package/dist/EditableProductCard.d.ts +7 -0
- package/dist/EditableProductCard.js +3 -9
- package/dist/EditableProductDetail.d.ts +1 -0
- package/dist/EditableProductDetail.js +6 -8
- package/dist/EditableServiceCard.d.ts +26 -1
- package/dist/EditableServiceCard.js +76 -11
- package/dist/EditableUniversalCard.d.ts +5 -3
- package/dist/PlatformProductDetail.js +3 -8
- package/dist/ProductQuickView.d.ts +2 -0
- package/dist/ProductQuickView.js +3 -2
- package/dist/SiteDataProvider.d.ts +40 -2
- package/dist/SiteDataProvider.js +248 -7
- package/dist/utils/catalogMedia.d.ts +18 -0
- package/dist/utils/catalogMedia.js +43 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -637,6 +637,12 @@ export function PromoSection() {
|
|
|
637
637
|
|
|
638
638
|
DENEB UI commerce components natively support both **Single Fixed Prices** and **Dynamic Price Ranges**, as well as apparel clothing options (color swatches with image swapping and size chips).
|
|
639
639
|
|
|
640
|
+
Fivora uses one canonical catalog contract: `ProductCard` is the commerce
|
|
641
|
+
renderer, `ServiceCard` is the service/quote renderer, and `UniversalCard` is
|
|
642
|
+
reserved for other generic entity listings. Keep `imageUrl` as the primary
|
|
643
|
+
image and `additionalImages` as the ordered gallery; DENEB normalizes them to
|
|
644
|
+
`images`/`gallery` for detail and quick-view components.
|
|
645
|
+
|
|
640
646
|
Keep `price`, `compareAtPrice`, `minPrice`, `maxPrice`, and variant prices as
|
|
641
647
|
JSON numbers. Keep `currency` or a currency symbol as separate text. A formatted
|
|
642
648
|
`priceRange` string is display-only; numeric filters and sorting must use the
|
|
@@ -653,7 +659,8 @@ import { ProductCard } from "@deneb-ui/ui";
|
|
|
653
659
|
price: 3500,
|
|
654
660
|
compareAtPrice: 4500,
|
|
655
661
|
currency: "LKR",
|
|
656
|
-
imageUrl: "/images/shirt.jpg"
|
|
662
|
+
imageUrl: "/images/shirt.jpg",
|
|
663
|
+
additionalImages: ["/images/shirt-back.jpg", "/images/shirt-detail.jpg"]
|
|
657
664
|
}}
|
|
658
665
|
itemPath="products[0]"
|
|
659
666
|
/>
|
|
@@ -673,6 +680,7 @@ import { ProductCard } from "@deneb-ui/ui";
|
|
|
673
680
|
isPriceRange: true,
|
|
674
681
|
currency: "LKR",
|
|
675
682
|
imageUrl: "/images/tunic-navy.jpg",
|
|
683
|
+
additionalImages: ["/images/tunic-back.jpg", "/images/tunic-detail.jpg"],
|
|
676
684
|
colors: [
|
|
677
685
|
{ name: "Navy Blue", hex: "#1e3a8a", imageUrl: "/images/tunic-navy.jpg" },
|
|
678
686
|
{ name: "Olive Green", hex: "#3f6212", imageUrl: "/images/tunic-olive.jpg" },
|
|
@@ -29,12 +29,19 @@ export interface ProductItem {
|
|
|
29
29
|
productImage?: string;
|
|
30
30
|
gallery?: string[];
|
|
31
31
|
images?: string[];
|
|
32
|
+
additionalImages?: string[];
|
|
32
33
|
badge?: string;
|
|
33
34
|
tag?: string;
|
|
34
35
|
rating?: number | string;
|
|
35
36
|
reviewsCount?: number | string;
|
|
36
37
|
isNew?: boolean;
|
|
37
38
|
isBestSeller?: boolean;
|
|
39
|
+
isAvailable?: boolean;
|
|
40
|
+
inStock?: boolean;
|
|
41
|
+
available?: boolean;
|
|
42
|
+
isActive?: boolean;
|
|
43
|
+
active?: boolean;
|
|
44
|
+
status?: string;
|
|
38
45
|
whatsappNumber?: string;
|
|
39
46
|
whatsappButtonText?: string;
|
|
40
47
|
addToCartButtonText?: string;
|
|
@@ -9,6 +9,7 @@ const EditableImage_1 = require("./EditableImage");
|
|
|
9
9
|
const useCart_1 = require("./cart/useCart");
|
|
10
10
|
const productOptions_1 = require("./utils/productOptions");
|
|
11
11
|
const PlatformProductDetail_1 = require("./PlatformProductDetail");
|
|
12
|
+
const catalogMedia_1 = require("./utils/catalogMedia");
|
|
12
13
|
const COLOR_HEX_MAP = {
|
|
13
14
|
black: '#111827',
|
|
14
15
|
white: '#ffffff',
|
|
@@ -166,14 +167,7 @@ function EditableProductCard({ itemPath, product: productProp, item: itemProp, c
|
|
|
166
167
|
const badge = String(product?.badge || product?.tag || '');
|
|
167
168
|
const hasBadgeField = Boolean(product && ('badge' in product || 'tag' in product));
|
|
168
169
|
const fallbackImage = imageFallback || 'https://images.unsplash.com/photo-1542291026-7eec264c27ff?auto=format&fit=crop&w=800&q=80';
|
|
169
|
-
const
|
|
170
|
-
product?.image ||
|
|
171
|
-
product?.photo ||
|
|
172
|
-
product?.thumbnail ||
|
|
173
|
-
product?.productImage ||
|
|
174
|
-
(Array.isArray(product?.gallery) && product.gallery[0]) ||
|
|
175
|
-
(Array.isArray(product?.images) && product.images[0]);
|
|
176
|
-
const baseImageUrl = String(rawImage || fallbackImage);
|
|
170
|
+
const baseImageUrl = (0, catalogMedia_1.resolveCatalogPrimaryImage)(product, fallbackImage);
|
|
177
171
|
// Active image: clicked color image -> resolved variant image -> product base image
|
|
178
172
|
const currentImage = colorImage || resolvedPrice.variantImage || baseImageUrl;
|
|
179
173
|
const imageKey = 'image' in (product || {}) && !('imageUrl' in (product || {}) && product.imageUrl)
|
|
@@ -457,7 +451,7 @@ function EditableProductCard({ itemPath, product: productProp, item: itemProp, c
|
|
|
457
451
|
fontSize: '0.8rem',
|
|
458
452
|
fontWeight: 500,
|
|
459
453
|
color: 'var(--muted-text, #64748b)',
|
|
460
|
-
} })) : null] })) : !effectiveShowPrice ? ((0, jsx_runtime_1.jsx)("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' }, children: (0, jsx_runtime_1.jsx)("span", { style: {
|
|
454
|
+
} })) : null] })) : (!effectiveShowPrice || !hasPrice) ? ((0, jsx_runtime_1.jsx)("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem' }, children: (0, jsx_runtime_1.jsx)("span", { style: {
|
|
461
455
|
display: 'inline-flex',
|
|
462
456
|
alignItems: 'center',
|
|
463
457
|
padding: '4px 10px',
|
|
@@ -9,6 +9,7 @@ const react_1 = require("react");
|
|
|
9
9
|
const utils_1 = require("./utils");
|
|
10
10
|
const productOptions_1 = require("./utils/productOptions");
|
|
11
11
|
const urls_1 = require("./utils/urls");
|
|
12
|
+
const catalogMedia_1 = require("./utils/catalogMedia");
|
|
12
13
|
/**
|
|
13
14
|
* EditableProductDetail is an elite, fully calibrated Single Product View component
|
|
14
15
|
* engineered for high-conversion commerce and 100% compliant with Fivora Visual Editing.
|
|
@@ -28,14 +29,11 @@ function EditableProductDetail({ sectionPath = 'product', product, sizes, colors
|
|
|
28
29
|
const productRecord = (product || {});
|
|
29
30
|
const productPhoto = productRecord.photo;
|
|
30
31
|
const productThumbnail = productRecord.thumbnail;
|
|
31
|
-
const validImages = (0, react_1.useMemo)(() => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
: [product.featuredImage || product.imageUrl || product.image || productPhoto || productThumbnail || '/products/vanta-aero-x.jpg'];
|
|
37
|
-
return rawList.filter(Boolean);
|
|
38
|
-
}, [product.gallery, product.images, product.featuredImage, product.imageUrl, product.image, productPhoto, productThumbnail]);
|
|
32
|
+
const validImages = (0, react_1.useMemo)(() => (0, catalogMedia_1.resolveCatalogImages)({
|
|
33
|
+
...product,
|
|
34
|
+
photo: productPhoto,
|
|
35
|
+
thumbnail: productThumbnail,
|
|
36
|
+
}, '/products/vanta-aero-x.jpg'), [product, productPhoto, productThumbnail]);
|
|
39
37
|
// 2. Resolve Options (Sizes, Volumes, Weights, Counts)
|
|
40
38
|
const resolved = (0, productOptions_1.resolveProductOptions)(product);
|
|
41
39
|
const effectiveOptions = (0, react_1.useMemo)(() => {
|
|
@@ -4,13 +4,30 @@ export interface ServiceItem {
|
|
|
4
4
|
name?: string;
|
|
5
5
|
title?: string;
|
|
6
6
|
price?: string | number;
|
|
7
|
+
cost?: string | number;
|
|
8
|
+
amount?: string | number;
|
|
9
|
+
rate?: string | number;
|
|
10
|
+
serviceFee?: string | number;
|
|
11
|
+
servicePrice?: string | number;
|
|
12
|
+
compareAtPrice?: string | number;
|
|
13
|
+
originalPrice?: string | number;
|
|
7
14
|
priceLabel?: string;
|
|
8
15
|
showPrice?: boolean;
|
|
16
|
+
currency?: string;
|
|
9
17
|
customData?: Record<string, unknown>;
|
|
10
18
|
description?: string;
|
|
11
19
|
imageUrl?: string;
|
|
12
20
|
image?: string;
|
|
21
|
+
gallery?: string[];
|
|
22
|
+
images?: string[];
|
|
23
|
+
additionalImages?: string[];
|
|
13
24
|
features?: unknown[];
|
|
25
|
+
badge?: string;
|
|
26
|
+
tag?: string;
|
|
27
|
+
isActive?: boolean;
|
|
28
|
+
active?: boolean;
|
|
29
|
+
isAvailable?: boolean;
|
|
30
|
+
available?: boolean;
|
|
14
31
|
[key: string]: unknown;
|
|
15
32
|
}
|
|
16
33
|
export interface EditableServiceCardProps extends React.HTMLAttributes<HTMLElement> {
|
|
@@ -35,9 +52,17 @@ export interface EditableServiceCardProps extends React.HTMLAttributes<HTMLEleme
|
|
|
35
52
|
*/
|
|
36
53
|
align?: 'left' | 'center' | 'right';
|
|
37
54
|
showPrice?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Whether to show active/inactive status badge (default: true).
|
|
57
|
+
*/
|
|
58
|
+
showStatusBadge?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Currency code override (e.g. "LKR", "USD"). If omitted, auto-resolves from live shop profile.
|
|
61
|
+
*/
|
|
62
|
+
currency?: string;
|
|
38
63
|
}
|
|
39
64
|
/**
|
|
40
65
|
* EditableServiceCard standardizes service presentation across Home and Services pages.
|
|
41
66
|
* Supports service name, description, image, and repeatable features checklist.
|
|
42
67
|
*/
|
|
43
|
-
export declare function EditableServiceCard({ itemPath, service, imageFallback, as: Component, align, showPrice, className, style, children, ...props }: EditableServiceCardProps): React.JSX.Element;
|
|
68
|
+
export declare function EditableServiceCard({ itemPath, service, imageFallback, as: Component, align, showPrice, showStatusBadge, currency: currencyOverride, className, style, children, ...props }: EditableServiceCardProps): React.JSX.Element;
|
|
@@ -4,11 +4,18 @@ exports.EditableServiceCard = EditableServiceCard;
|
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const EditableText_1 = require("./EditableText");
|
|
6
6
|
const EditableImage_1 = require("./EditableImage");
|
|
7
|
+
const catalogMedia_1 = require("./utils/catalogMedia");
|
|
8
|
+
const SiteDataProvider_1 = require("./SiteDataProvider");
|
|
7
9
|
/**
|
|
8
10
|
* EditableServiceCard standardizes service presentation across Home and Services pages.
|
|
9
11
|
* Supports service name, description, image, and repeatable features checklist.
|
|
10
12
|
*/
|
|
11
|
-
function EditableServiceCard({ itemPath, service, imageFallback = '/placeholder.svg', as: Component = 'article', align = 'left', showPrice = true, className = '', style, children, ...props }) {
|
|
13
|
+
function EditableServiceCard({ itemPath, service, imageFallback = '/placeholder.svg', as: Component = 'article', align = 'left', showPrice = true, showStatusBadge = true, currency: currencyOverride, className = '', style, children, ...props }) {
|
|
14
|
+
const isServiceActive = service?.isActive !== false &&
|
|
15
|
+
service?.active !== false &&
|
|
16
|
+
service?.isAvailable !== false &&
|
|
17
|
+
service?.available !== false;
|
|
18
|
+
const badgeCandidate = service?.badge || service?.tag;
|
|
12
19
|
const name = String(service?.name || service?.title || '');
|
|
13
20
|
const description = String(service?.description || '');
|
|
14
21
|
const itemShowPrice = service.showPrice !== undefined
|
|
@@ -21,37 +28,95 @@ function EditableServiceCard({ itemPath, service, imageFallback = '/placeholder.
|
|
|
21
28
|
const hasRawPrice = rawPriceCandidate !== undefined && rawPriceCandidate !== null && String(rawPriceCandidate).trim() !== '';
|
|
22
29
|
const priceLabel = service?.priceLabel ? String(service.priceLabel) : '';
|
|
23
30
|
const hasPrice = effectiveShowPrice && (hasRawPrice || Boolean(priceLabel));
|
|
31
|
+
const liveCurrency = (0, SiteDataProvider_1.useCurrency)();
|
|
32
|
+
const currency = currencyOverride || service?.currency || liveCurrency;
|
|
24
33
|
const formattedPrice = hasRawPrice
|
|
25
34
|
? typeof rawPriceCandidate === 'number'
|
|
26
|
-
? '
|
|
27
|
-
: String(rawPriceCandidate).includes(
|
|
35
|
+
? currency + ' ' + rawPriceCandidate.toLocaleString()
|
|
36
|
+
: String(rawPriceCandidate).match(/^[A-Z]{3}\s/) || String(rawPriceCandidate).includes(currency)
|
|
28
37
|
? String(rawPriceCandidate)
|
|
29
|
-
: '
|
|
38
|
+
: currency + ' ' + String(rawPriceCandidate)
|
|
30
39
|
: '';
|
|
31
|
-
const
|
|
40
|
+
const rawCompareCandidate = service?.compareAtPrice || service?.originalPrice;
|
|
41
|
+
const hasComparePrice = effectiveShowPrice &&
|
|
42
|
+
rawCompareCandidate !== undefined &&
|
|
43
|
+
rawCompareCandidate !== null &&
|
|
44
|
+
String(rawCompareCandidate).trim() !== '' &&
|
|
45
|
+
Number(rawCompareCandidate) > Number(rawPriceCandidate);
|
|
46
|
+
const formattedComparePrice = hasComparePrice
|
|
47
|
+
? typeof rawCompareCandidate === 'number'
|
|
48
|
+
? currency + ' ' + rawCompareCandidate.toLocaleString()
|
|
49
|
+
: String(rawCompareCandidate).match(/^[A-Z]{3}\s/) || String(rawCompareCandidate).includes(currency)
|
|
50
|
+
? String(rawCompareCandidate)
|
|
51
|
+
: currency + ' ' + String(rawCompareCandidate)
|
|
52
|
+
: '';
|
|
53
|
+
const serviceImages = (0, catalogMedia_1.resolveCatalogImages)(service);
|
|
54
|
+
const imageUrl = serviceImages[0] || '';
|
|
32
55
|
const features = Array.isArray(service?.features) ? service.features : [];
|
|
33
56
|
const mergedStyle = {
|
|
34
57
|
textAlign: align,
|
|
58
|
+
opacity: !isServiceActive ? 0.72 : 1,
|
|
35
59
|
...style,
|
|
36
60
|
};
|
|
37
|
-
|
|
61
|
+
const badgeElement = (badgeCandidate || (!isServiceActive && showStatusBadge)) ? ((0, jsx_runtime_1.jsx)("div", { className: "service-card-badge-wrap", style: {
|
|
62
|
+
position: 'absolute',
|
|
63
|
+
top: '0.75rem',
|
|
64
|
+
left: '0.75rem',
|
|
65
|
+
zIndex: 3,
|
|
66
|
+
display: 'flex',
|
|
67
|
+
gap: '0.375rem',
|
|
68
|
+
pointerEvents: 'none',
|
|
69
|
+
}, children: !isServiceActive && showStatusBadge ? ((0, jsx_runtime_1.jsxs)("span", { className: "service-card-status-badge inactive", style: {
|
|
70
|
+
display: 'inline-flex',
|
|
71
|
+
alignItems: 'center',
|
|
72
|
+
gap: '0.25rem',
|
|
73
|
+
padding: '0.25rem 0.625rem',
|
|
74
|
+
borderRadius: '9999px',
|
|
75
|
+
fontSize: '0.6875rem',
|
|
76
|
+
fontWeight: 700,
|
|
77
|
+
textTransform: 'uppercase',
|
|
78
|
+
letterSpacing: '0.05em',
|
|
79
|
+
backgroundColor: 'rgba(239, 68, 68, 0.95)',
|
|
80
|
+
color: '#ffffff',
|
|
81
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
|
82
|
+
}, children: [(0, jsx_runtime_1.jsx)("span", { style: { width: 6, height: 6, borderRadius: '50%', backgroundColor: 'currentColor' } }), "Unavailable"] })) : badgeCandidate ? ((0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "span", id: `${itemPath}.badge`, "data-preview-field-path": `${itemPath}.badge`, defaultValue: String(badgeCandidate), className: "service-card-badge", style: {
|
|
83
|
+
display: 'inline-flex',
|
|
84
|
+
alignItems: 'center',
|
|
85
|
+
padding: '0.25rem 0.625rem',
|
|
86
|
+
borderRadius: '9999px',
|
|
87
|
+
fontSize: '0.6875rem',
|
|
88
|
+
fontWeight: 700,
|
|
89
|
+
textTransform: 'uppercase',
|
|
90
|
+
letterSpacing: '0.05em',
|
|
91
|
+
backgroundColor: 'var(--brand-color, #2563eb)',
|
|
92
|
+
color: '#ffffff',
|
|
93
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.15)',
|
|
94
|
+
pointerEvents: 'auto',
|
|
95
|
+
} })) : null })) : null;
|
|
96
|
+
return ((0, jsx_runtime_1.jsxs)(Component, { "data-preview-item-path": itemPath, className: `editable-service-card ${!isServiceActive ? 'is-inactive is-unavailable' : ''} ${className}`.trim(), style: mergedStyle, ...props, children: [(imageUrl || service?.imageUrl !== undefined || service?.image !== undefined) ? ((0, jsx_runtime_1.jsxs)("div", { className: "service-card-image-wrap", style: { position: 'relative' }, children: [badgeElement, (0, jsx_runtime_1.jsx)(EditableImage_1.EditableImage, { id: `${itemPath}.imageUrl`, "data-preview-field-path": `${itemPath}.imageUrl`, src: imageUrl, fallbackSrc: imageFallback, alt: name, className: "service-card-image" })] })) : badgeElement ? ((0, jsx_runtime_1.jsx)("div", { style: { position: 'relative', width: '100%', height: '1.75rem', marginBottom: '0.5rem' }, children: badgeElement })) : null, (0, jsx_runtime_1.jsxs)("div", { className: "service-card-body", children: [(0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "h2", id: `${itemPath}.name`, "data-preview-field-path": `${itemPath}.name`, defaultValue: name, className: "service-card-title" }), (0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "p", id: `${itemPath}.description`, "data-preview-field-path": `${itemPath}.description`, defaultValue: description, className: "service-card-description" }), hasPrice ? ((0, jsx_runtime_1.jsxs)("div", { className: "service-card-price-wrap", style: { margin: '0.5rem 0', display: 'flex', alignItems: 'baseline', gap: '0.5rem', flexWrap: 'wrap' }, children: [hasRawPrice && ((0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "span", id: `${itemPath}.price`, "data-preview-field-path": `${itemPath}.price`, defaultValue: formattedPrice, className: "service-card-price", style: {
|
|
38
97
|
fontSize: '1.125rem',
|
|
39
98
|
fontWeight: 700,
|
|
40
99
|
color: 'var(--brand-color, #2563eb)',
|
|
41
|
-
} })),
|
|
100
|
+
} })), hasComparePrice && ((0, jsx_runtime_1.jsx)("span", { className: "service-card-compare-price", style: {
|
|
101
|
+
fontSize: '0.875rem',
|
|
102
|
+
fontWeight: 500,
|
|
103
|
+
color: 'var(--muted-text, #94a3b8)',
|
|
104
|
+
textDecoration: 'line-through',
|
|
105
|
+
}, children: formattedComparePrice })), priceLabel && ((0, jsx_runtime_1.jsx)(EditableText_1.EditableText, { as: "span", id: `${itemPath}.priceLabel`, "data-preview-field-path": `${itemPath}.priceLabel`, defaultValue: priceLabel, className: "service-card-price-label", style: {
|
|
42
106
|
fontSize: '0.8125rem',
|
|
43
107
|
fontWeight: 500,
|
|
44
108
|
color: 'var(--muted-text, #94a3b8)',
|
|
45
|
-
} }))] })) : !effectiveShowPrice ? ((0, jsx_runtime_1.jsx)("div", { className: "service-card-price-wrap", style: { margin: '0.5rem 0' }, children: (0, jsx_runtime_1.jsx)("span", { style: {
|
|
109
|
+
} }))] })) : (!effectiveShowPrice || (!hasRawPrice && !priceLabel)) ? ((0, jsx_runtime_1.jsx)("div", { className: "service-card-price-wrap", style: { margin: '0.5rem 0' }, children: (0, jsx_runtime_1.jsx)("span", { style: {
|
|
46
110
|
display: 'inline-flex',
|
|
47
111
|
alignItems: 'center',
|
|
48
|
-
padding: '
|
|
49
|
-
borderRadius: '
|
|
50
|
-
fontSize: '0.
|
|
112
|
+
padding: '4px 10px',
|
|
113
|
+
borderRadius: '8px',
|
|
114
|
+
fontSize: '0.8125rem',
|
|
51
115
|
fontWeight: 600,
|
|
52
116
|
backgroundColor: 'rgba(255, 255, 255, 0.08)',
|
|
53
117
|
color: 'var(--muted-text, #94a3b8)',
|
|
54
118
|
border: '1px solid rgba(255, 255, 255, 0.12)',
|
|
119
|
+
letterSpacing: '0.01em',
|
|
55
120
|
}, children: "Quote on Request" }) })) : null, (features.length > 0 || Array.isArray(service?.features)) && ((0, jsx_runtime_1.jsx)("ul", { "data-preview-list-path": `${itemPath}.features`, className: "service-card-features", children: features.map((feature, featureIndex) => {
|
|
56
121
|
const featureText = typeof feature === 'string'
|
|
57
122
|
? feature
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* EditableUniversalCard — DENEB UI Framework
|
|
3
3
|
*
|
|
4
|
-
* Universal, multi-industry listing
|
|
4
|
+
* Universal, multi-industry listing renderer for non-catalog entities. Fivora
|
|
5
|
+
* products and services should use EditableProductCard/EditableServiceCard so
|
|
6
|
+
* their backend catalog behavior is preserved:
|
|
5
7
|
* - 🚗 Automotive & Equipment: Make/model, year & condition, mileage, fuel, gearbox, price
|
|
6
8
|
* - 📚 Bookshops & Publishers: Cover photo, book title, author, published year, pages, price
|
|
7
9
|
* - 🏋️ Gyms & Fitness: Membership tier, duration, meal plan, 24/7 access, trainer, price
|
|
8
|
-
* - 🍽️
|
|
10
|
+
* - 🍽️ Editorial Menus: Dish photo, title, dietary tags, ingredients, prep time, price
|
|
9
11
|
* - 🏠 Real Estate & Rentals: Property image, location, beds, baths, sqft, price
|
|
10
|
-
* - 💼
|
|
12
|
+
* - 💼 Agency Portfolios: Offering title, duration, deliverable checklist, pricing
|
|
11
13
|
*
|
|
12
14
|
* Designed with strict editability architecture:
|
|
13
15
|
* 1. Automatically resolves fields from `useSiteData()` via `itemPath` or explicit field paths.
|
|
@@ -9,6 +9,7 @@ const react_1 = require("react");
|
|
|
9
9
|
const EditableProductDetail_1 = require("./EditableProductDetail");
|
|
10
10
|
const SiteDataProvider_1 = require("./SiteDataProvider");
|
|
11
11
|
const utils_1 = require("./utils");
|
|
12
|
+
const catalogMedia_1 = require("./utils/catalogMedia");
|
|
12
13
|
const DEFAULT_QUERY_PARAM = 'id';
|
|
13
14
|
const DEFAULT_DETAIL_ROUTE = '/products/detail/';
|
|
14
15
|
const DEFAULT_RETRY_DELAYS = [0, 750, 2_000];
|
|
@@ -93,13 +94,7 @@ function normalizeProductForDetail(product, siteData) {
|
|
|
93
94
|
(typeof mergedProduct.thumbnail === 'string' && mergedProduct.thumbnail) ||
|
|
94
95
|
undefined;
|
|
95
96
|
const price = mergedProduct.price ?? mergedProduct.cost ?? mergedProduct.amount ?? mergedProduct.productPrice;
|
|
96
|
-
const images =
|
|
97
|
-
? mergedProduct.gallery
|
|
98
|
-
: Array.isArray(mergedProduct.images)
|
|
99
|
-
? mergedProduct.images
|
|
100
|
-
: Array.isArray(mergedProduct.additionalImages)
|
|
101
|
-
? mergedProduct.additionalImages.filter((value) => typeof value === 'string' && Boolean(value.trim()))
|
|
102
|
-
: undefined;
|
|
97
|
+
const images = (0, catalogMedia_1.resolveCatalogImages)(mergedProduct);
|
|
103
98
|
const compareAtPrice = mergedProduct.compareAtPrice ?? mergedProduct.originalPrice;
|
|
104
99
|
const shop = (0, SiteDataProvider_1.isRecord)(siteData.shop) ? siteData.shop : {};
|
|
105
100
|
const merchant = (0, SiteDataProvider_1.isRecord)(siteData.merchant) ? siteData.merchant : {};
|
|
@@ -122,7 +117,7 @@ function normalizeProductForDetail(product, siteData) {
|
|
|
122
117
|
? { featuredImage: image, imageUrl: image, image }
|
|
123
118
|
: {}),
|
|
124
119
|
...(price !== undefined ? { price } : {}),
|
|
125
|
-
...(images ? { gallery: images, images } : {}),
|
|
120
|
+
...(images.length > 0 ? { gallery: images, images } : {}),
|
|
126
121
|
...(compareAtPrice !== undefined
|
|
127
122
|
? { compareAtPrice, originalPrice: compareAtPrice }
|
|
128
123
|
: {}),
|
package/dist/ProductQuickView.js
CHANGED
|
@@ -5,6 +5,7 @@ const jsx_runtime_1 = require("react/jsx-runtime");
|
|
|
5
5
|
const react_1 = require("react");
|
|
6
6
|
const utils_1 = require("./utils");
|
|
7
7
|
const PlatformProductDetail_1 = require("./PlatformProductDetail");
|
|
8
|
+
const catalogMedia_1 = require("./utils/catalogMedia");
|
|
8
9
|
/**
|
|
9
10
|
* DENEB UI — Product Quick View Modal
|
|
10
11
|
*
|
|
@@ -20,7 +21,7 @@ function ProductQuickView({ product, isOpen, onClose, onAddToCart, className = '
|
|
|
20
21
|
(0, react_1.useEffect)(() => {
|
|
21
22
|
if (product) {
|
|
22
23
|
setQuantity(1);
|
|
23
|
-
setSelectedImage(
|
|
24
|
+
setSelectedImage((0, catalogMedia_1.resolveCatalogImages)(product)[0] || '');
|
|
24
25
|
}
|
|
25
26
|
}, [product]);
|
|
26
27
|
// Handle ESC key to close
|
|
@@ -42,7 +43,7 @@ function ProductQuickView({ product, isOpen, onClose, onAddToCart, className = '
|
|
|
42
43
|
if (!isOpen || !product) {
|
|
43
44
|
return null;
|
|
44
45
|
}
|
|
45
|
-
const images =
|
|
46
|
+
const images = (0, catalogMedia_1.resolveCatalogImages)(product);
|
|
46
47
|
const currency = product.currency || '$';
|
|
47
48
|
const isAvailable = product.isAvailable !== false && product.inStock !== false;
|
|
48
49
|
const handleAddToCart = () => {
|
|
@@ -112,14 +112,24 @@ export declare function formatWeeklyHoursToString(value: unknown): string;
|
|
|
112
112
|
export declare function normalizeProductItem(p: unknown): ProductItem;
|
|
113
113
|
/**
|
|
114
114
|
* Universal Service Normalizer
|
|
115
|
+
* Ensures every service object has both canonical and alias keys simultaneously populated
|
|
116
|
+
* with clean numbers and resolved image URLs, matching the product normalizer's robustness.
|
|
115
117
|
*/
|
|
116
118
|
export declare function normalizeServiceItem(s: unknown): ServiceItem;
|
|
117
|
-
export
|
|
119
|
+
export interface UseProductsOptions {
|
|
120
|
+
inStockOnly?: boolean;
|
|
121
|
+
activeOnly?: boolean;
|
|
122
|
+
}
|
|
123
|
+
export declare function useProducts(fallback?: ProductItem[], options?: UseProductsOptions): ProductItem[];
|
|
124
|
+
export interface UseServicesOptions {
|
|
125
|
+
activeOnly?: boolean;
|
|
126
|
+
}
|
|
118
127
|
/**
|
|
119
128
|
* Hook to retrieve services cleanly from SiteData, supporting both
|
|
120
129
|
* top-level content.services and nested content.home.services.
|
|
130
|
+
* Supports optional { activeOnly: true } filtering to mirror product availability.
|
|
121
131
|
*/
|
|
122
|
-
export declare function useServices(fallback?: ServiceItem[]): ServiceItem[];
|
|
132
|
+
export declare function useServices(fallback?: ServiceItem[], options?: UseServicesOptions): ServiceItem[];
|
|
123
133
|
/**
|
|
124
134
|
* Hook to access official Fivora backend API endpoints (catalogUrl, contactUrl, analyticsUrl).
|
|
125
135
|
*/
|
|
@@ -178,3 +188,31 @@ export interface SyncedBranding {
|
|
|
178
188
|
* Hierarchy: Visual Editor Edits (content.common) -> Live Merchant Shop Profile -> Template Fallback.
|
|
179
189
|
*/
|
|
180
190
|
export declare function useSyncedBranding(fallback?: Partial<SyncedBranding>): SyncedBranding;
|
|
191
|
+
/**
|
|
192
|
+
* Hook to retrieve the live store currency code (e.g. "LKR", "USD", "EUR").
|
|
193
|
+
* Auto-hydrates from the merchant portal's shop profile.
|
|
194
|
+
* Hierarchy: shop.currency → content.common.currency → merchant.currency → fallback.
|
|
195
|
+
*/
|
|
196
|
+
export declare function useCurrency(fallback?: string): string;
|
|
197
|
+
export interface SocialLinksMap {
|
|
198
|
+
facebook: string;
|
|
199
|
+
instagram: string;
|
|
200
|
+
tiktok: string;
|
|
201
|
+
youtube: string;
|
|
202
|
+
linkedin: string;
|
|
203
|
+
twitter: string;
|
|
204
|
+
website: string;
|
|
205
|
+
[key: string]: string;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Hook to retrieve the live social links map from the merchant profile.
|
|
209
|
+
* Auto-hydrates from the shop owner's Fivora Portal social configuration.
|
|
210
|
+
* Returns a map like { facebook: "https://...", instagram: "https://...", ... }.
|
|
211
|
+
*/
|
|
212
|
+
export declare function useSocial(fallback?: Partial<SocialLinksMap>): SocialLinksMap;
|
|
213
|
+
/**
|
|
214
|
+
* Hook to retrieve the live shop front / cover / banner image URL.
|
|
215
|
+
* Auto-hydrates from the merchant portal's shop profile.
|
|
216
|
+
* Hierarchy: shop.coverImage → shop.shopImage → shop.bannerImage → shop.heroImage → content.common.shopImage → fallback.
|
|
217
|
+
*/
|
|
218
|
+
export declare function useShopImage(fallback?: string): string;
|
package/dist/SiteDataProvider.js
CHANGED
|
@@ -32,6 +32,9 @@ exports.isPreviewValidation = isPreviewValidation;
|
|
|
32
32
|
exports.useCommon = useCommon;
|
|
33
33
|
exports.useContact = useContact;
|
|
34
34
|
exports.useSyncedBranding = useSyncedBranding;
|
|
35
|
+
exports.useCurrency = useCurrency;
|
|
36
|
+
exports.useSocial = useSocial;
|
|
37
|
+
exports.useShopImage = useShopImage;
|
|
35
38
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
36
39
|
const useCart_1 = require("./cart/useCart");
|
|
37
40
|
const react_1 = require("react");
|
|
@@ -43,6 +46,7 @@ const ThemeStyles_1 = require("./ThemeStyles");
|
|
|
43
46
|
const utils_1 = require("./utils");
|
|
44
47
|
Object.defineProperty(exports, "withBasePath", { enumerable: true, get: function () { return utils_1.withBasePath; } });
|
|
45
48
|
Object.defineProperty(exports, "resolveAsset", { enumerable: true, get: function () { return utils_1.resolveAsset; } });
|
|
49
|
+
const catalogMedia_1 = require("./utils/catalogMedia");
|
|
46
50
|
exports.DENEB_PREVIEW_DATA_MESSAGE = 'DENEB_PREVIEW_SITE_DATA';
|
|
47
51
|
exports.PREVIEW_DATA_MESSAGE = 'FIVORA_PREVIEW_SITE_DATA';
|
|
48
52
|
const PREVIOUS_PREVIEW_PREFIX = `${['MARKET', 'PLACE'].join('')}_PREVIEW_`;
|
|
@@ -875,13 +879,36 @@ function normalizeProductItem(p) {
|
|
|
875
879
|
p.src ??
|
|
876
880
|
p.productImage ??
|
|
877
881
|
(Array.isArray(p.gallery) ? p.gallery[0] : undefined) ??
|
|
878
|
-
(Array.isArray(p.images) ? p.images[0] : undefined)
|
|
882
|
+
(Array.isArray(p.images) ? p.images[0] : undefined) ??
|
|
883
|
+
(Array.isArray(p.additionalImages) ? p.additionalImages[0] : undefined);
|
|
884
|
+
const catalogImages = (0, catalogMedia_1.resolveCatalogImages)(p);
|
|
879
885
|
const badgeCandidate = p.badge ?? p.tag ?? p.label ?? p.badgeText ?? p.chip;
|
|
880
886
|
const descCandidate = p.description ?? p.desc ?? p.details ?? p.shortDescription ?? p.subtitle;
|
|
881
887
|
const explicitMin = p.minPrice ?? customData.minPrice;
|
|
882
888
|
const explicitMax = p.maxPrice ?? customData.maxPrice;
|
|
883
889
|
const explicitRange = p.priceRange ?? customData.priceRange;
|
|
884
890
|
const isRange = Boolean(p.isPriceRange || customData.isPriceRange || (explicitMax && explicitMin));
|
|
891
|
+
const isAvailableCandidate = p.isAvailable !== undefined
|
|
892
|
+
? Boolean(p.isAvailable)
|
|
893
|
+
: p.available !== undefined
|
|
894
|
+
? Boolean(p.available)
|
|
895
|
+
: p.inStock !== undefined
|
|
896
|
+
? Boolean(p.inStock)
|
|
897
|
+
: customData.isAvailable !== undefined
|
|
898
|
+
? Boolean(customData.isAvailable)
|
|
899
|
+
: true;
|
|
900
|
+
const isActiveCandidate = p.isActive !== undefined
|
|
901
|
+
? Boolean(p.isActive)
|
|
902
|
+
: p.active !== undefined
|
|
903
|
+
? Boolean(p.active)
|
|
904
|
+
: customData.isActive !== undefined
|
|
905
|
+
? Boolean(customData.isActive)
|
|
906
|
+
: true;
|
|
907
|
+
const showPriceCandidate = p.showPrice !== undefined
|
|
908
|
+
? Boolean(p.showPrice)
|
|
909
|
+
: customData.showPrice !== undefined
|
|
910
|
+
? Boolean(customData.showPrice)
|
|
911
|
+
: true;
|
|
885
912
|
return {
|
|
886
913
|
...customData,
|
|
887
914
|
...p,
|
|
@@ -889,6 +916,12 @@ function normalizeProductItem(p) {
|
|
|
889
916
|
title: titleCandidate,
|
|
890
917
|
productName: (titleCandidate ?? nameCandidate),
|
|
891
918
|
itemTitle: (titleCandidate ?? nameCandidate),
|
|
919
|
+
isAvailable: isAvailableCandidate,
|
|
920
|
+
inStock: isAvailableCandidate,
|
|
921
|
+
available: isAvailableCandidate,
|
|
922
|
+
isActive: isActiveCandidate,
|
|
923
|
+
active: isActiveCandidate,
|
|
924
|
+
showPrice: showPriceCandidate,
|
|
892
925
|
minPrice: explicitMin !== undefined ? explicitMin : undefined,
|
|
893
926
|
priceMin: (explicitMin ?? p.priceMin) !== undefined ? (explicitMin ?? p.priceMin) : undefined,
|
|
894
927
|
maxPrice: explicitMax !== undefined ? explicitMax : undefined,
|
|
@@ -918,6 +951,13 @@ function normalizeProductItem(p) {
|
|
|
918
951
|
productImage: imageCandidate,
|
|
919
952
|
}
|
|
920
953
|
: {}),
|
|
954
|
+
...(catalogImages.length > 0
|
|
955
|
+
? {
|
|
956
|
+
gallery: catalogImages,
|
|
957
|
+
images: catalogImages,
|
|
958
|
+
additionalImages: catalogImages.slice(1),
|
|
959
|
+
}
|
|
960
|
+
: {}),
|
|
921
961
|
...(badgeCandidate !== undefined
|
|
922
962
|
? {
|
|
923
963
|
badge: badgeCandidate,
|
|
@@ -935,6 +975,8 @@ function normalizeProductItem(p) {
|
|
|
935
975
|
}
|
|
936
976
|
/**
|
|
937
977
|
* Universal Service Normalizer
|
|
978
|
+
* Ensures every service object has both canonical and alias keys simultaneously populated
|
|
979
|
+
* with clean numbers and resolved image URLs, matching the product normalizer's robustness.
|
|
938
980
|
*/
|
|
939
981
|
function normalizeServiceItem(s) {
|
|
940
982
|
if (!isRecord(s))
|
|
@@ -943,17 +985,126 @@ function normalizeServiceItem(s) {
|
|
|
943
985
|
const nameCandidate = s.name ?? s.title ?? s.serviceName ?? s.heading;
|
|
944
986
|
const titleCandidate = s.title ?? s.name ?? s.serviceName ?? s.heading;
|
|
945
987
|
const descCandidate = s.description ?? s.desc ?? s.details ?? s.shortDescription;
|
|
946
|
-
const imageCandidate = s.imageUrl ??
|
|
988
|
+
const imageCandidate = s.imageUrl ??
|
|
989
|
+
s.image ??
|
|
990
|
+
s.photo ??
|
|
991
|
+
s.thumbnail ??
|
|
992
|
+
s.picture ??
|
|
993
|
+
(Array.isArray(s.gallery) ? s.gallery[0] : undefined) ??
|
|
994
|
+
(Array.isArray(s.images) ? s.images[0] : undefined) ??
|
|
995
|
+
(Array.isArray(s.additionalImages) ? s.additionalImages[0] : undefined);
|
|
996
|
+
const catalogImages = (0, catalogMedia_1.resolveCatalogImages)(s);
|
|
997
|
+
// Price normalization — parse string prices to clean numbers, support aliases
|
|
998
|
+
const rawPrice = s.price !== undefined
|
|
999
|
+
? s.price
|
|
1000
|
+
: s.cost !== undefined
|
|
1001
|
+
? s.cost
|
|
1002
|
+
: s.amount !== undefined
|
|
1003
|
+
? s.amount
|
|
1004
|
+
: s.rate !== undefined
|
|
1005
|
+
? s.rate
|
|
1006
|
+
: s.serviceFee !== undefined
|
|
1007
|
+
? s.serviceFee
|
|
1008
|
+
: s.servicePrice !== undefined
|
|
1009
|
+
? s.servicePrice
|
|
1010
|
+
: customData.price !== undefined
|
|
1011
|
+
? customData.price
|
|
1012
|
+
: undefined;
|
|
1013
|
+
let numPrice = rawPrice;
|
|
1014
|
+
if (typeof rawPrice === "string" && rawPrice.trim()) {
|
|
1015
|
+
const isRangeStr = rawPrice.includes("-") || rawPrice.includes("–") || rawPrice.includes("—") || /\bto\b/i.test(rawPrice);
|
|
1016
|
+
if (!isRangeStr) {
|
|
1017
|
+
const match = rawPrice.match(/-?\d+(?:,\d{3})*(?:\.\d+)?|-?\d+(?:\.\d+)?/);
|
|
1018
|
+
if (match) {
|
|
1019
|
+
const parsed = parseFloat(match[0].replace(/,/g, ""));
|
|
1020
|
+
if (!isNaN(parsed) && Number.isFinite(parsed)) {
|
|
1021
|
+
numPrice = parsed;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
// Compare/original price normalization for services
|
|
1027
|
+
const rawCompare = s.compareAtPrice !== undefined
|
|
1028
|
+
? s.compareAtPrice
|
|
1029
|
+
: s.originalPrice !== undefined
|
|
1030
|
+
? s.originalPrice
|
|
1031
|
+
: s.oldPrice !== undefined
|
|
1032
|
+
? s.oldPrice
|
|
1033
|
+
: s.regularPrice !== undefined
|
|
1034
|
+
? s.regularPrice
|
|
1035
|
+
: customData.compareAtPrice !== undefined
|
|
1036
|
+
? customData.compareAtPrice
|
|
1037
|
+
: undefined;
|
|
1038
|
+
let numCompare = rawCompare;
|
|
1039
|
+
if (typeof rawCompare === "string" && rawCompare.trim()) {
|
|
1040
|
+
const match = rawCompare.match(/-?\d+(?:,\d{3})*(?:\.\d+)?|-?\d+(?:\.\d+)?/);
|
|
1041
|
+
if (match) {
|
|
1042
|
+
const parsed = parseFloat(match[0].replace(/,/g, ""));
|
|
1043
|
+
if (!isNaN(parsed) && Number.isFinite(parsed)) {
|
|
1044
|
+
numCompare = parsed;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const priceLabelCandidate = s.priceLabel ?? customData.priceLabel;
|
|
1049
|
+
const isActiveCandidate = s.isActive !== undefined
|
|
1050
|
+
? Boolean(s.isActive)
|
|
1051
|
+
: s.active !== undefined
|
|
1052
|
+
? Boolean(s.active)
|
|
1053
|
+
: s.isAvailable !== undefined
|
|
1054
|
+
? Boolean(s.isAvailable)
|
|
1055
|
+
: s.available !== undefined
|
|
1056
|
+
? Boolean(s.available)
|
|
1057
|
+
: customData.isActive !== undefined
|
|
1058
|
+
? Boolean(customData.isActive)
|
|
1059
|
+
: customData.isAvailable !== undefined
|
|
1060
|
+
? Boolean(customData.isAvailable)
|
|
1061
|
+
: true;
|
|
1062
|
+
const showPriceCandidate = s.showPrice !== undefined
|
|
1063
|
+
? Boolean(s.showPrice)
|
|
1064
|
+
: customData.showPrice !== undefined
|
|
1065
|
+
? Boolean(customData.showPrice)
|
|
1066
|
+
: true;
|
|
1067
|
+
const badgeCandidate = s.badge ?? s.tag ?? s.label ?? customData.badge ?? customData.tag;
|
|
947
1068
|
return {
|
|
948
1069
|
...customData,
|
|
949
1070
|
...s,
|
|
950
1071
|
name: nameCandidate,
|
|
951
1072
|
title: titleCandidate,
|
|
1073
|
+
isActive: isActiveCandidate,
|
|
1074
|
+
active: isActiveCandidate,
|
|
1075
|
+
isAvailable: isActiveCandidate,
|
|
1076
|
+
available: isActiveCandidate,
|
|
1077
|
+
showPrice: showPriceCandidate,
|
|
952
1078
|
...(descCandidate !== undefined ? { description: descCandidate, desc: descCandidate, details: descCandidate } : {}),
|
|
953
1079
|
...(imageCandidate !== undefined ? { imageUrl: imageCandidate, image: imageCandidate, photo: imageCandidate } : {}),
|
|
1080
|
+
...(badgeCandidate !== undefined ? { badge: String(badgeCandidate), tag: String(badgeCandidate) } : {}),
|
|
1081
|
+
...(catalogImages.length > 0
|
|
1082
|
+
? {
|
|
1083
|
+
gallery: catalogImages,
|
|
1084
|
+
images: catalogImages,
|
|
1085
|
+
additionalImages: catalogImages.slice(1),
|
|
1086
|
+
}
|
|
1087
|
+
: {}),
|
|
1088
|
+
...(numPrice !== undefined
|
|
1089
|
+
? {
|
|
1090
|
+
price: numPrice,
|
|
1091
|
+
cost: numPrice,
|
|
1092
|
+
amount: numPrice,
|
|
1093
|
+
rate: numPrice,
|
|
1094
|
+
serviceFee: numPrice,
|
|
1095
|
+
servicePrice: numPrice,
|
|
1096
|
+
}
|
|
1097
|
+
: {}),
|
|
1098
|
+
...(numCompare !== undefined
|
|
1099
|
+
? {
|
|
1100
|
+
compareAtPrice: numCompare,
|
|
1101
|
+
originalPrice: numCompare,
|
|
1102
|
+
}
|
|
1103
|
+
: {}),
|
|
1104
|
+
...(priceLabelCandidate !== undefined ? { priceLabel: priceLabelCandidate } : {}),
|
|
954
1105
|
};
|
|
955
1106
|
}
|
|
956
|
-
function useProducts(fallback = []) {
|
|
1107
|
+
function useProducts(fallback = [], options) {
|
|
957
1108
|
const siteData = useSiteData();
|
|
958
1109
|
const content = isRecord(siteData?.content) ? siteData.content : null;
|
|
959
1110
|
if (!content)
|
|
@@ -979,17 +1130,26 @@ function useProducts(fallback = []) {
|
|
|
979
1130
|
}
|
|
980
1131
|
if (!rawList)
|
|
981
1132
|
return fallback;
|
|
982
|
-
|
|
1133
|
+
let list = rawList.map(normalizeProductItem);
|
|
1134
|
+
if (options?.activeOnly) {
|
|
1135
|
+
list = list.filter((p) => p.isActive !== false && p.status !== 'ARCHIVED' && p.status !== 'DRAFT');
|
|
1136
|
+
}
|
|
1137
|
+
if (options?.inStockOnly) {
|
|
1138
|
+
list = list.filter((p) => p.isAvailable !== false && p.inStock !== false);
|
|
1139
|
+
}
|
|
1140
|
+
return list;
|
|
983
1141
|
}
|
|
984
1142
|
/**
|
|
985
1143
|
* Hook to retrieve services cleanly from SiteData, supporting both
|
|
986
1144
|
* top-level content.services and nested content.home.services.
|
|
1145
|
+
* Supports optional { activeOnly: true } filtering to mirror product availability.
|
|
987
1146
|
*/
|
|
988
|
-
function useServices(fallback = []) {
|
|
1147
|
+
function useServices(fallback = [], options) {
|
|
989
1148
|
const siteData = useSiteData();
|
|
990
1149
|
const content = isRecord(siteData?.content) ? siteData.content : null;
|
|
991
1150
|
if (!content)
|
|
992
1151
|
return fallback;
|
|
1152
|
+
let rawList = null;
|
|
993
1153
|
const candidates = [
|
|
994
1154
|
content.services,
|
|
995
1155
|
content.servicesPage?.services,
|
|
@@ -1000,10 +1160,15 @@ function useServices(fallback = []) {
|
|
|
1000
1160
|
];
|
|
1001
1161
|
for (const candidate of candidates) {
|
|
1002
1162
|
if (Array.isArray(candidate) && candidate.length > 0) {
|
|
1003
|
-
|
|
1163
|
+
rawList = candidate;
|
|
1164
|
+
break;
|
|
1004
1165
|
}
|
|
1005
1166
|
}
|
|
1006
|
-
|
|
1167
|
+
const list = rawList ? rawList.map(normalizeServiceItem) : fallback;
|
|
1168
|
+
if (options?.activeOnly) {
|
|
1169
|
+
return list.filter((s) => s.isActive !== false && s.isAvailable !== false);
|
|
1170
|
+
}
|
|
1171
|
+
return list;
|
|
1007
1172
|
}
|
|
1008
1173
|
/**
|
|
1009
1174
|
* Hook to access official Fivora backend API endpoints (catalogUrl, contactUrl, analyticsUrl).
|
|
@@ -1230,3 +1395,79 @@ function useSyncedBranding(fallback = {}) {
|
|
|
1230
1395
|
faviconUrl,
|
|
1231
1396
|
};
|
|
1232
1397
|
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Hook to retrieve the live store currency code (e.g. "LKR", "USD", "EUR").
|
|
1400
|
+
* Auto-hydrates from the merchant portal's shop profile.
|
|
1401
|
+
* Hierarchy: shop.currency → content.common.currency → merchant.currency → fallback.
|
|
1402
|
+
*/
|
|
1403
|
+
function useCurrency(fallback = 'LKR') {
|
|
1404
|
+
const siteData = useSiteData();
|
|
1405
|
+
const content = isRecord(siteData?.content) ? siteData.content : null;
|
|
1406
|
+
const common = isRecord(content?.common) ? content.common : {};
|
|
1407
|
+
const shop = useShop();
|
|
1408
|
+
return ((typeof shop.currency === 'string' && shop.currency.trim()) ||
|
|
1409
|
+
(typeof common.currency === 'string' && common.currency.trim()) ||
|
|
1410
|
+
(typeof shop.currencyCode === 'string' && shop.currencyCode.trim()) ||
|
|
1411
|
+
(typeof common.currencyCode === 'string' && common.currencyCode.trim()) ||
|
|
1412
|
+
fallback);
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Hook to retrieve the live social links map from the merchant profile.
|
|
1416
|
+
* Auto-hydrates from the shop owner's Fivora Portal social configuration.
|
|
1417
|
+
* Returns a map like { facebook: "https://...", instagram: "https://...", ... }.
|
|
1418
|
+
*/
|
|
1419
|
+
function useSocial(fallback = {}) {
|
|
1420
|
+
const siteData = useSiteData();
|
|
1421
|
+
const content = isRecord(siteData?.content) ? siteData.content : null;
|
|
1422
|
+
const common = isRecord(content?.common) ? content.common : {};
|
|
1423
|
+
const shop = useShop();
|
|
1424
|
+
const shopSocial = isRecord(shop.socialLinks) ? shop.socialLinks : (isRecord(shop.social) ? shop.social : {});
|
|
1425
|
+
const commonSocial = isRecord(common.socialLinks) ? common.socialLinks : (isRecord(common.social) ? common.social : {});
|
|
1426
|
+
const extractString = (key) => {
|
|
1427
|
+
const candidates = [
|
|
1428
|
+
shopSocial[key],
|
|
1429
|
+
commonSocial[key],
|
|
1430
|
+
shop[key],
|
|
1431
|
+
common[key],
|
|
1432
|
+
fallback[key],
|
|
1433
|
+
];
|
|
1434
|
+
for (const candidate of candidates) {
|
|
1435
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
1436
|
+
return candidate.trim();
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return '';
|
|
1440
|
+
};
|
|
1441
|
+
return {
|
|
1442
|
+
facebook: extractString('facebook') || extractString('facebookUrl'),
|
|
1443
|
+
instagram: extractString('instagram') || extractString('instagramUrl'),
|
|
1444
|
+
tiktok: extractString('tiktok') || extractString('tiktokUrl'),
|
|
1445
|
+
youtube: extractString('youtube') || extractString('youtubeUrl'),
|
|
1446
|
+
linkedin: extractString('linkedin') || extractString('linkedinUrl'),
|
|
1447
|
+
twitter: extractString('twitter') || extractString('twitterUrl') || extractString('x'),
|
|
1448
|
+
website: extractString('website') || extractString('websiteUrl'),
|
|
1449
|
+
...Object.fromEntries(Object.entries({ ...fallback, ...commonSocial, ...shopSocial })
|
|
1450
|
+
.filter(([, v]) => typeof v === 'string' && v.trim())
|
|
1451
|
+
.map(([k, v]) => [k, String(v).trim()])),
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Hook to retrieve the live shop front / cover / banner image URL.
|
|
1456
|
+
* Auto-hydrates from the merchant portal's shop profile.
|
|
1457
|
+
* Hierarchy: shop.coverImage → shop.shopImage → shop.bannerImage → shop.heroImage → content.common.shopImage → fallback.
|
|
1458
|
+
*/
|
|
1459
|
+
function useShopImage(fallback = '') {
|
|
1460
|
+
const siteData = useSiteData();
|
|
1461
|
+
const content = isRecord(siteData?.content) ? siteData.content : null;
|
|
1462
|
+
const common = isRecord(content?.common) ? content.common : {};
|
|
1463
|
+
const shop = useShop();
|
|
1464
|
+
return ((typeof shop.coverImage === 'string' && shop.coverImage.trim()) ||
|
|
1465
|
+
(typeof shop.shopImage === 'string' && shop.shopImage.trim()) ||
|
|
1466
|
+
(typeof shop.bannerImage === 'string' && shop.bannerImage.trim()) ||
|
|
1467
|
+
(typeof shop.heroImage === 'string' && shop.heroImage.trim()) ||
|
|
1468
|
+
(typeof shop.shopFrontImage === 'string' && shop.shopFrontImage.trim()) ||
|
|
1469
|
+
(typeof common.shopImage === 'string' && common.shopImage.trim()) ||
|
|
1470
|
+
(typeof common.coverImage === 'string' && common.coverImage.trim()) ||
|
|
1471
|
+
(typeof common.bannerImage === 'string' && common.bannerImage.trim()) ||
|
|
1472
|
+
fallback);
|
|
1473
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface CatalogMediaItem {
|
|
2
|
+
featuredImage?: unknown;
|
|
3
|
+
imageUrl?: unknown;
|
|
4
|
+
image?: unknown;
|
|
5
|
+
photo?: unknown;
|
|
6
|
+
thumbnail?: unknown;
|
|
7
|
+
productImage?: unknown;
|
|
8
|
+
gallery?: unknown;
|
|
9
|
+
images?: unknown;
|
|
10
|
+
additionalImages?: unknown;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Resolves Fivora's catalog media contract while retaining legacy template
|
|
15
|
+
* aliases. The primary image stays first and duplicate URLs are removed.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveCatalogImages(item?: CatalogMediaItem | null, fallbackImage?: string): string[];
|
|
18
|
+
export declare function resolveCatalogPrimaryImage(item?: CatalogMediaItem | null, fallbackImage?: string): string;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveCatalogImages = resolveCatalogImages;
|
|
4
|
+
exports.resolveCatalogPrimaryImage = resolveCatalogPrimaryImage;
|
|
5
|
+
function appendImage(images, seen, candidate) {
|
|
6
|
+
if (typeof candidate !== 'string')
|
|
7
|
+
return;
|
|
8
|
+
const normalized = candidate.trim();
|
|
9
|
+
if (!normalized || seen.has(normalized))
|
|
10
|
+
return;
|
|
11
|
+
seen.add(normalized);
|
|
12
|
+
images.push(normalized);
|
|
13
|
+
}
|
|
14
|
+
function appendImageList(images, seen, candidate) {
|
|
15
|
+
if (!Array.isArray(candidate))
|
|
16
|
+
return;
|
|
17
|
+
candidate.forEach((value) => appendImage(images, seen, value));
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Resolves Fivora's catalog media contract while retaining legacy template
|
|
21
|
+
* aliases. The primary image stays first and duplicate URLs are removed.
|
|
22
|
+
*/
|
|
23
|
+
function resolveCatalogImages(item, fallbackImage) {
|
|
24
|
+
const images = [];
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const source = item || {};
|
|
27
|
+
appendImage(images, seen, source.featuredImage);
|
|
28
|
+
appendImage(images, seen, source.imageUrl);
|
|
29
|
+
appendImage(images, seen, source.image);
|
|
30
|
+
appendImage(images, seen, source.photo);
|
|
31
|
+
appendImage(images, seen, source.thumbnail);
|
|
32
|
+
appendImage(images, seen, source.productImage);
|
|
33
|
+
appendImageList(images, seen, source.gallery);
|
|
34
|
+
appendImageList(images, seen, source.images);
|
|
35
|
+
appendImageList(images, seen, source.additionalImages);
|
|
36
|
+
if (images.length === 0) {
|
|
37
|
+
appendImage(images, seen, fallbackImage);
|
|
38
|
+
}
|
|
39
|
+
return images;
|
|
40
|
+
}
|
|
41
|
+
function resolveCatalogPrimaryImage(item, fallbackImage) {
|
|
42
|
+
return resolveCatalogImages(item, fallbackImage)[0] || '';
|
|
43
|
+
}
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deneb-ui/ui",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.128",
|
|
4
4
|
"description": "Visual-first React component library for editable commerce storefronts. Built for Next.js and Fivora.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
],
|
|
51
51
|
"license": "MIT",
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@deneb-ui/core": "^2.0.
|
|
53
|
+
"@deneb-ui/core": "^2.0.128"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"react": "^18.0.0 || ^19.0.0",
|