@zenky/storefront-vue 0.10.18 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +107 -0
- package/dist/cart/index.js +6 -6
- package/dist/chunk-6HOKVW4M.js +134 -0
- package/dist/chunk-6HOKVW4M.js.map +1 -0
- package/dist/{chunk-LABBB43Q.js → chunk-CQDOEP6T.js} +163 -128
- package/dist/chunk-CQDOEP6T.js.map +1 -0
- package/dist/{chunk-6BXHWR5E.js → chunk-IEHN2WNU.js} +46 -13
- package/dist/chunk-IEHN2WNU.js.map +1 -0
- package/dist/chunk-JY3UJRMF.js +59 -0
- package/dist/chunk-JY3UJRMF.js.map +1 -0
- package/dist/{chunk-YTYCSTHP.js → chunk-PBBVWWMI.js} +3 -3
- package/dist/{chunk-YTYCSTHP.js.map → chunk-PBBVWWMI.js.map} +1 -1
- package/dist/{chunk-2DUKIVKC.js → chunk-QFKA2R6D.js} +3 -3
- package/dist/{chunk-2DUKIVKC.js.map → chunk-QFKA2R6D.js.map} +1 -1
- package/dist/{chunk-T52G6JN7.js → chunk-TXR76IDL.js} +16 -3
- package/dist/chunk-TXR76IDL.js.map +1 -0
- package/dist/{chunk-RURXGVWW.js → chunk-UJKWKD3W.js} +4 -56
- package/dist/chunk-UJKWKD3W.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +13 -11
- package/dist/products/index.d.ts +42 -2
- package/dist/products/index.js +3 -2
- package/dist/promotions/index.js +2 -2
- package/dist/seo/index.d.ts +44 -16
- package/dist/seo/index.js +338 -104
- package/dist/seo/index.js.map +1 -1
- package/dist/stores/index.d.ts +3 -2
- package/dist/stores/index.js +3 -3
- package/dist/utils/index.js +2 -1
- package/docs/architecture.md +5 -3
- package/docs/consumer-setup.md +1 -1
- package/package.json +3 -3
- package/dist/chunk-6BXHWR5E.js.map +0 -1
- package/dist/chunk-LABBB43Q.js.map +0 -1
- package/dist/chunk-RURXGVWW.js.map +0 -1
- package/dist/chunk-T52G6JN7.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -206,6 +206,113 @@ All notable changes to this project are documented in this file. The format foll
|
|
|
206
206
|
`register()` returning `true` left the user believing they were signed in when they
|
|
207
207
|
weren't. This path now performs a real `login()` with the same password instead.
|
|
208
208
|
|
|
209
|
+
## 0.12.0 - 2026-10-19
|
|
210
|
+
|
|
211
|
+
Needs `@zenky/storefront-api` `^1.7.0` (peer dependency).
|
|
212
|
+
|
|
213
|
+
Changes the result of `buildOrganizationSchema`: one `@graph` instead of a single
|
|
214
|
+
`LocalBusiness` node, see the first entry under Changed.
|
|
215
|
+
|
|
216
|
+
### Added
|
|
217
|
+
|
|
218
|
+
- `buildLocationsSchema` (`/seo`) — `@graph` with the store's `Organization` and one node
|
|
219
|
+
per stock that has an address, for a page that lists the chain's locations. Each
|
|
220
|
+
location gets a page-independent `@id` (`{origin}/#location-{stockId}`, the fragment is
|
|
221
|
+
`locationAnchorId(stockId)` so a consumer can put the same id on the rendered card),
|
|
222
|
+
`parentOrganization`, a `PostalAddress` whose `streetAddress` drops the city already
|
|
223
|
+
carried by `addressLocality` (a leading "г {city}," or a trailing ", {city}"),
|
|
224
|
+
`addressCountry` from `store.settings.country`, `geo` plus a Yandex Maps `hasMap` link,
|
|
225
|
+
the first phone of the stock → city → store, the store logo as `image`, and an optional
|
|
226
|
+
`url` from `options.locationUrl(stock)`. Names lose zero-width characters and repeated
|
|
227
|
+
spaces.
|
|
228
|
+
- Location `@type` (`BUSINESS_TYPES`, `isBusinessType`, `BusinessType`): without
|
|
229
|
+
`options.businessType` it is guessed from the stock name — «ресторан», «кафе», «бар»,
|
|
230
|
+
«паб», «пиццерия», «кофейня», «бистро» and their English counterparts give
|
|
231
|
+
`Restaurant`, anything else `LocalBusiness`; an explicit type applies to every location
|
|
232
|
+
except the ones named as pickup/issue points («самовывоз», «выдача», "pick-up"), which
|
|
233
|
+
stay `LocalBusiness`.
|
|
234
|
+
- `buildBreadcrumbListSchema` (`/seo`) — `BreadcrumbList` from `BreadcrumbItem[]`, paths
|
|
235
|
+
resolved against the origin, empty labels dropped. The category and collection builders
|
|
236
|
+
use it.
|
|
237
|
+
- `buildOrganizationReference` (`/seo`) — a minimal `Organization` node with the same
|
|
238
|
+
`@id`, for `offers.seller` or any link to the store from a page that does not carry the
|
|
239
|
+
full graph.
|
|
240
|
+
- Schema id helpers (`/seo`): `organizationSchemaId`, `webSiteSchemaId`,
|
|
241
|
+
`locationSchemaId`, `locationAnchorId`, `productSchemaId`.
|
|
242
|
+
- `normalizeSchemaText` / `htmlToPlainText` (`/seo`) — plain text for structured data and
|
|
243
|
+
microdata: invisible characters removed, whitespace collapsed; the HTML variant keeps
|
|
244
|
+
words from adjacent blocks apart and decodes common entities.
|
|
245
|
+
- `buildProductOffer` (`/seo`) — the `Offer` / `AggregateOffer` of the product JSON-LD as a
|
|
246
|
+
typed record (`ProductOffer`), so microdata rendered next to it repeats exactly the same
|
|
247
|
+
price and availability.
|
|
248
|
+
- `getRequiredModifiersMinCost` (`/products`) — the cheapest mandatory modifier cost,
|
|
249
|
+
extracted from `useProductPricing` so the card price and the `Offer` price come from the
|
|
250
|
+
same code.
|
|
251
|
+
|
|
252
|
+
### Changed
|
|
253
|
+
|
|
254
|
+
- `buildOrganizationSchema(store, origin, options?)` returns one `@graph` —
|
|
255
|
+
`Organization` (`{origin}/#organization`: name, url, logo, phones, email, `sameAs`, city
|
|
256
|
+
address for a single-city store), `WebSite` (`publisher` → the organization, optional
|
|
257
|
+
`inLanguage`), and the location nodes described above. It used to return a single
|
|
258
|
+
`LocalBusiness` built from the first stock of the default city, so a chain of seven
|
|
259
|
+
locations was described as one of them; a store without an addressed stock now gets a
|
|
260
|
+
plain `Organization`. Consumers that read fields of the returned node must switch to
|
|
261
|
+
`@graph`.
|
|
262
|
+
- `buildProductSchema(product, currency, options?)` takes `{ url, stockId, seller }`:
|
|
263
|
+
`@id` (`{url}#product`), `url`, `offers.url`, `offers.seller`; the price resolves for
|
|
264
|
+
the given stock and includes the mandatory modifiers minimum, exactly like the product
|
|
265
|
+
card. `availability` is always `InStock`: a storefront renders only what the resolved
|
|
266
|
+
stock catalog sells and the backend checks sellability at checkout, while the old guess
|
|
267
|
+
from `variant.remainders` marked orderable products `OutOfStock`. Adds `sku`,
|
|
268
|
+
`category`, and `weight` (`QuantitativeValue`, `GRM`/`KGM`), strips HTML from
|
|
269
|
+
`description`, uses an `AggregateOffer` (`lowPrice`/`highPrice`) for several variants,
|
|
270
|
+
and omits `offers` when the price is zero instead of advertising the product as free.
|
|
271
|
+
Without options the output keeps its previous shape apart from the plain-text
|
|
272
|
+
description and the constant availability.
|
|
273
|
+
- Opening hours in location nodes: days with identical hours are grouped into one
|
|
274
|
+
`OpeningHoursSpecification` with a `dayOfWeek` array, split shifts produce one entry
|
|
275
|
+
each (the old builder kept only the first shift), round-the-clock days ("00:00-00:00",
|
|
276
|
+
«круглосуточно», "24/7", «24 часа») become 00:00–23:59, "24:00" closes at 23:59, and
|
|
277
|
+
malformed times are skipped.
|
|
278
|
+
|
|
279
|
+
## 0.11.0 - 2026-10-07
|
|
280
|
+
|
|
281
|
+
Needs `@zenky/storefront-api` `^1.7.0` (peer dependency).
|
|
282
|
+
|
|
283
|
+
### Added
|
|
284
|
+
|
|
285
|
+
- Order packaging: `ORDER_WITH` now includes `packages`, and `useCartStore` gets
|
|
286
|
+
`setOrderPackages(packages, packagingPrice)`. Add/remove responses don't carry
|
|
287
|
+
`packages`, so `useCartOperations` reloads them after the total is recalculated, but
|
|
288
|
+
only when the checkout total reports `packaging_price > 0`; at zero a leftover list is
|
|
289
|
+
cleared locally without a request. Late or out-of-order responses (an older sync, or a
|
|
290
|
+
previous order) are dropped, and a failed reload is logged without breaking the
|
|
291
|
+
add/remove. Needs `@zenky/storefront-api` with `Order.packages`,
|
|
292
|
+
`Order.packaging_price` and `OrderCheckoutTotal.packaging_price`.
|
|
293
|
+
- Merchant product badges (`/products`): `toProductBadgeViews` validates the backend
|
|
294
|
+
`Product.badges` list — drops badges with nothing to show, repeated ids and unusable
|
|
295
|
+
icons, accepts only hex colors (alpha is ignored) and http(s) image urls, and returns
|
|
296
|
+
`colors: null` when either color is missing or invalid so the consumer can fall back to
|
|
297
|
+
its theme. `getProductBadgeLayout` tells a text badge from an image without a label (drawn
|
|
298
|
+
as a square with no background), `filterProductBadges` hides either kind, and
|
|
299
|
+
`getProductBadgeAccent` derives a palette from the badge's own text/background pair
|
|
300
|
+
(accent, readable `on` color, hue-shifted gradient end, readable text, translucent glass
|
|
301
|
+
fill) with plain JS math, no CSS color functions. Styles, sizes and placement stay with
|
|
302
|
+
the consumer. Needs `@zenky/storefront-api` with `Product.badges` (`^1.6.0`).
|
|
303
|
+
|
|
304
|
+
### Changed
|
|
305
|
+
|
|
306
|
+
- `useCartStore.setOrder` keeps the previous `packages` (and `packaging_price`, unless the
|
|
307
|
+
new order brings its own) when the new order has the same id and no `packages`;
|
|
308
|
+
responses such as `setOrderPayments` omit them, and replacing the order wholesale made
|
|
309
|
+
the packaging block disappear while the total still included it.
|
|
310
|
+
- `addToCart` now forwards `promotionId` / `promotionRewardId` to the API as
|
|
311
|
+
`promotion_id` / `promotion_reward_id`, so promotion gifts are added through the regular
|
|
312
|
+
pipeline (total recalculation, packaging sync, promotion re-check) instead of a direct
|
|
313
|
+
SDK call. Gift adds do not emit add-to-cart analytics events (`productAddedToCart`,
|
|
314
|
+
`productQuantityIncreased`, `trackAddedToCart`); removal events are unchanged.
|
|
315
|
+
|
|
209
316
|
## 0.10.7 - 2026-08-02
|
|
210
317
|
|
|
211
318
|
### Added
|
package/dist/cart/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
export { CASH_DENOMINATIONS_MAJOR, DEFAULT_ROUND_STEPS_MAJOR, ORDER_EVENTS, ORDER_WITH, buildCartLineKey, buildCashChangePresets, buildMainPaymentRequest, getCashDenominationsMinor, getDefaultRoundStepsMinor, normalizeAddress, useBonusesReconciliation, useCartAddressSync, useCartCustomerAttachment, useCartDeliveryAttachment, useCartInitialization, useCartMutation, useCartOperations, useCartQuantities, useCartRecalculation, useCartStockWatcher, useCheckoutFlow, useCheckoutSubmit, useCustomerAttach, useOnlinePaymentRetry, useOrderBonuses, useOrderChannel, useOrderDelivery, useOrderPromocode, useOrderSettings, useOrderTotal, usePaymentSelection, useRestrictedAddToCart, useRestrictionGate, useStorefrontCart, useVisibleCartVariants, variantLineKey } from '../chunk-
|
|
2
|
-
export { useCartCredentials } from '../chunk-
|
|
3
|
-
import '../chunk-5QCUH3FF.js';
|
|
1
|
+
export { CASH_DENOMINATIONS_MAJOR, DEFAULT_ROUND_STEPS_MAJOR, ORDER_EVENTS, ORDER_WITH, buildCartLineKey, buildCashChangePresets, buildMainPaymentRequest, getCashDenominationsMinor, getDefaultRoundStepsMinor, normalizeAddress, useBonusesReconciliation, useCartAddressSync, useCartCustomerAttachment, useCartDeliveryAttachment, useCartInitialization, useCartMutation, useCartOperations, useCartQuantities, useCartRecalculation, useCartStockWatcher, useCheckoutFlow, useCheckoutSubmit, useCustomerAttach, useOnlinePaymentRetry, useOrderBonuses, useOrderChannel, useOrderDelivery, useOrderPromocode, useOrderSettings, useOrderTotal, usePaymentSelection, useRestrictedAddToCart, useRestrictionGate, useStorefrontCart, useVisibleCartVariants, variantLineKey } from '../chunk-IEHN2WNU.js';
|
|
2
|
+
export { useCartCredentials } from '../chunk-QFKA2R6D.js';
|
|
4
3
|
export { getCartErrorCode, getCartErrorHttpCode, getCartErrorMessage, getValidationErrors, logCartError } from '../chunk-TE42OCFK.js';
|
|
5
4
|
import '../chunk-544USTPB.js';
|
|
6
|
-
import '../chunk-4JCJHAL3.js';
|
|
7
|
-
import '../chunk-T52G6JN7.js';
|
|
8
5
|
import '../chunk-TIQ7AJLY.js';
|
|
9
|
-
import '../chunk-IOYYCA75.js';
|
|
10
6
|
import '../chunk-FS2UNPTY.js';
|
|
11
7
|
import '../chunk-2Y6QTHXI.js';
|
|
8
|
+
import '../chunk-5QCUH3FF.js';
|
|
9
|
+
import '../chunk-4JCJHAL3.js';
|
|
10
|
+
import '../chunk-TXR76IDL.js';
|
|
11
|
+
import '../chunk-IOYYCA75.js';
|
|
12
12
|
//# sourceMappingURL=index.js.map
|
|
13
13
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { UnitType } from '@zenky/storefront-api';
|
|
2
|
+
import { computed } from 'vue';
|
|
3
|
+
|
|
4
|
+
// src/products/formatWeight.ts
|
|
5
|
+
var UNIT_LABELS = {
|
|
6
|
+
[UnitType.Gram]: "\u0433",
|
|
7
|
+
[UnitType.Kilogram]: "\u043A\u0433",
|
|
8
|
+
[UnitType.Meter]: "\u043C",
|
|
9
|
+
[UnitType.Piece]: "\u0433"
|
|
10
|
+
};
|
|
11
|
+
function getWeightFromDimensions(variants) {
|
|
12
|
+
if (!variants?.length) return null;
|
|
13
|
+
const dimension = variants[0]?.dimensions?.find((d) => d.dimension === "weight");
|
|
14
|
+
return dimension?.value ?? null;
|
|
15
|
+
}
|
|
16
|
+
function getVariantWeight(variant) {
|
|
17
|
+
if (!variant) return null;
|
|
18
|
+
return variant.dimensions?.find((d) => d.dimension === "weight")?.value ?? null;
|
|
19
|
+
}
|
|
20
|
+
function getProductWeight(product) {
|
|
21
|
+
return product.weight ?? getWeightFromDimensions(product.variants) ?? null;
|
|
22
|
+
}
|
|
23
|
+
function toGrams(weight, unitType) {
|
|
24
|
+
if (!weight) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
if (unitType === UnitType.Kilogram) {
|
|
28
|
+
return weight * 1e3;
|
|
29
|
+
}
|
|
30
|
+
if (unitType === UnitType.Meter) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
return weight;
|
|
34
|
+
}
|
|
35
|
+
function formatWeight(weight, unitType) {
|
|
36
|
+
if (!weight) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const label = UNIT_LABELS[unitType];
|
|
40
|
+
return `${weight} ${label}`;
|
|
41
|
+
}
|
|
42
|
+
function resolveVariantPrice(variant, stockId) {
|
|
43
|
+
if (variant.prices?.length) {
|
|
44
|
+
if (stockId) {
|
|
45
|
+
const stockEntry = variant.prices.find((p) => p.stock_id === stockId);
|
|
46
|
+
if (stockEntry) {
|
|
47
|
+
return {
|
|
48
|
+
price: stockEntry.price,
|
|
49
|
+
originalPrice: stockEntry.original_price,
|
|
50
|
+
discount: stockEntry.discount
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const globalEntry = variant.prices.find((p) => p.stock_id === null);
|
|
55
|
+
if (globalEntry) {
|
|
56
|
+
return {
|
|
57
|
+
price: globalEntry.price,
|
|
58
|
+
originalPrice: globalEntry.original_price,
|
|
59
|
+
discount: globalEntry.discount
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
price: variant.price,
|
|
65
|
+
originalPrice: variant.original_price ?? null,
|
|
66
|
+
discount: variant.discount ?? null
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function getRequiredModifiersMinCost(product) {
|
|
70
|
+
let total = 0;
|
|
71
|
+
for (const groupEntry of product.modifiers_groups ?? []) {
|
|
72
|
+
const group = groupEntry.group;
|
|
73
|
+
for (const modEntry of group.modifiers) {
|
|
74
|
+
if (modEntry.is_required && modEntry.modifier.price) {
|
|
75
|
+
total += modEntry.modifier.price * Math.max(modEntry.min_quantity ?? 0, 1);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (groupEntry.is_required && group.modifiers.length && !group.modifiers.some((m) => m.is_required)) {
|
|
79
|
+
const cheapest = group.modifiers.reduce(
|
|
80
|
+
(min, m) => (m.modifier.price ?? 0) < (min.modifier.price ?? 0) ? m : min
|
|
81
|
+
);
|
|
82
|
+
total += (cheapest.modifier.price ?? 0) * Math.max(cheapest.min_quantity ?? 0, 1);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return total;
|
|
86
|
+
}
|
|
87
|
+
function useProductPricing(product, stockId) {
|
|
88
|
+
const variants = computed(() => product.value.variants ?? []);
|
|
89
|
+
const currentStockId = computed(() => stockId?.value ?? null);
|
|
90
|
+
const resolvedPrices = computed(
|
|
91
|
+
() => variants.value.map((v) => resolveVariantPrice(v, currentStockId.value))
|
|
92
|
+
);
|
|
93
|
+
const minRequiredModifiersCost = computed(
|
|
94
|
+
() => getRequiredModifiersMinCost(product.value)
|
|
95
|
+
);
|
|
96
|
+
const displayPrice = computed(() => {
|
|
97
|
+
if (!resolvedPrices.value.length) {
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
return Math.min(...resolvedPrices.value.map((r) => r.price)) + minRequiredModifiersCost.value;
|
|
101
|
+
});
|
|
102
|
+
const originalDisplayPrice = computed(() => {
|
|
103
|
+
if (!resolvedPrices.value.length) return null;
|
|
104
|
+
const cheapest = resolvedPrices.value.reduce((min, r) => r.price < min.price ? r : min);
|
|
105
|
+
let originalVariantPrice = null;
|
|
106
|
+
if (cheapest.originalPrice && cheapest.originalPrice > cheapest.price) {
|
|
107
|
+
originalVariantPrice = cheapest.originalPrice;
|
|
108
|
+
} else if (cheapest.discount && cheapest.discount.difference > 0) {
|
|
109
|
+
originalVariantPrice = cheapest.price + cheapest.discount.difference;
|
|
110
|
+
}
|
|
111
|
+
if (!originalVariantPrice) return null;
|
|
112
|
+
return originalVariantPrice + minRequiredModifiersCost.value;
|
|
113
|
+
});
|
|
114
|
+
const hasDifferentPrices = computed(() => {
|
|
115
|
+
if (resolvedPrices.value.length <= 1) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const prices = new Set(resolvedPrices.value.map((r) => r.price));
|
|
119
|
+
return prices.size > 1;
|
|
120
|
+
});
|
|
121
|
+
const needsSelection = computed(() => {
|
|
122
|
+
if (variants.value.length > 1) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
const mods = product.value.modifiers;
|
|
126
|
+
const groups = product.value.modifiers_groups;
|
|
127
|
+
return Array.isArray(mods) && mods.length > 0 || Array.isArray(groups) && groups.length > 0;
|
|
128
|
+
});
|
|
129
|
+
return { displayPrice, originalDisplayPrice, hasDifferentPrices, needsSelection };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export { formatWeight, getProductWeight, getRequiredModifiersMinCost, getVariantWeight, resolveVariantPrice, toGrams, useProductPricing };
|
|
133
|
+
//# sourceMappingURL=chunk-6HOKVW4M.js.map
|
|
134
|
+
//# sourceMappingURL=chunk-6HOKVW4M.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/products/formatWeight.ts","../src/products/useProductPricing.ts"],"names":[],"mappings":"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","file":"chunk-6HOKVW4M.js","sourcesContent":["import { UnitType } from '@zenky/storefront-api'\n\nimport type { Product, ProductVariant } from '@zenky/storefront-api'\n\nconst UNIT_LABELS: Record<UnitType, string> = {\n [UnitType.Gram]: 'г',\n [UnitType.Kilogram]: 'кг',\n [UnitType.Meter]: 'м',\n [UnitType.Piece]: 'г',\n} as const\n\nfunction getWeightFromDimensions(variants: ProductVariant[] | undefined): number | null {\n if (!variants?.length) return null\n\n const dimension = variants[0]?.dimensions?.find((d) => d.dimension === 'weight')\n\n return dimension?.value ?? null\n}\n\nexport function getVariantWeight(variant: ProductVariant | null | undefined): number | null {\n if (!variant) return null\n\n return variant.dimensions?.find((d) => d.dimension === 'weight')?.value ?? null\n}\n\nexport function getProductWeight(product: Product): number | null {\n return product.weight ?? getWeightFromDimensions(product.variants) ?? null\n}\n\nexport function toGrams(weight: number | null, unitType: UnitType): number | null {\n if (!weight) {\n return null\n }\n\n if (unitType === UnitType.Kilogram) {\n return weight * 1000\n }\n\n if (unitType === UnitType.Meter) {\n return null\n }\n\n return weight\n}\n\nexport function formatWeight(weight: number | null, unitType: UnitType): string | null {\n if (!weight) {\n return null\n }\n\n const label = UNIT_LABELS[unitType]\n\n return `${weight} ${label}`\n}\n","import { computed, type ComputedRef } from 'vue'\n\nimport type { Discount, Product, ProductVariant } from '@zenky/storefront-api'\n\nexport interface ResolvedPrice {\n price: number\n originalPrice: number | null\n discount: Discount | null\n}\n\n/**\n * Resolves a variant's price, accounting for stock.\n * Priority: prices[stockId] → prices[null] → top-level price.\n */\nexport function resolveVariantPrice(\n variant: ProductVariant,\n stockId?: string | null\n): ResolvedPrice {\n if (variant.prices?.length) {\n // 1. Exact match by stock\n if (stockId) {\n const stockEntry = variant.prices.find((p) => p.stock_id === stockId)\n\n if (stockEntry) {\n return {\n price: stockEntry.price,\n originalPrice: stockEntry.original_price,\n discount: stockEntry.discount,\n }\n }\n }\n\n // 2. Global price (stock_id = null)\n const globalEntry = variant.prices.find((p) => p.stock_id === null)\n\n if (globalEntry) {\n return {\n price: globalEntry.price,\n originalPrice: globalEntry.original_price,\n discount: globalEntry.discount,\n }\n }\n }\n\n // 3. Fallback — the variant's top-level fields\n return {\n price: variant.price,\n originalPrice: variant.original_price ?? null,\n discount: variant.discount ?? null,\n }\n}\n\n// Minimum required modifier cost that's always added to the price:\n// 1. modEntry.is_required — the modifier is always sent to the API (can't be removed)\n// 2. groupEntry.is_required — the user must pick at least one from the group →\n// add the cheapest price in the group (if no is_required modifier already covers it)\nexport function getRequiredModifiersMinCost(product: Product): number {\n let total = 0\n\n for (const groupEntry of product.modifiers_groups ?? []) {\n const group = groupEntry.group\n\n for (const modEntry of group.modifiers) {\n if (modEntry.is_required && modEntry.modifier.price) {\n total += modEntry.modifier.price * Math.max(modEntry.min_quantity ?? 0, 1)\n }\n }\n\n if (\n groupEntry.is_required &&\n group.modifiers.length &&\n !group.modifiers.some((m) => m.is_required)\n ) {\n const cheapest = group.modifiers.reduce((min, m) =>\n (m.modifier.price ?? 0) < (min.modifier.price ?? 0) ? m : min\n )\n total += (cheapest.modifier.price ?? 0) * Math.max(cheapest.min_quantity ?? 0, 1)\n }\n }\n\n return total\n}\n\nexport function useProductPricing(\n product: ComputedRef<Product>,\n stockId?: ComputedRef<string | null | undefined>\n): {\n displayPrice: ComputedRef<number>\n originalDisplayPrice: ComputedRef<number | null>\n hasDifferentPrices: ComputedRef<boolean>\n needsSelection: ComputedRef<boolean>\n} {\n const variants = computed(() => product.value.variants ?? [])\n const currentStockId = computed(() => stockId?.value ?? null)\n\n const resolvedPrices = computed(() =>\n variants.value.map((v) => resolveVariantPrice(v, currentStockId.value))\n )\n\n const minRequiredModifiersCost = computed<number>(() =>\n getRequiredModifiersMinCost(product.value)\n )\n\n const displayPrice = computed<number>(() => {\n if (!resolvedPrices.value.length) {\n return 0\n }\n\n return (\n Math.min(...resolvedPrices.value.map((r) => r.price)) + minRequiredModifiersCost.value\n )\n })\n\n // Price without the discount: original_price of the cheapest variant + required modifiers.\n // null if there is no discount.\n // original_price can be null even when a discount exists — then compute it via discount.difference.\n const originalDisplayPrice = computed<number | null>(() => {\n if (!resolvedPrices.value.length) return null\n\n // Find the variant with the minimum price (the same one that determines displayPrice)\n const cheapest = resolvedPrices.value.reduce((min, r) => (r.price < min.price ? r : min))\n\n // Priority: original_price, then compute via discount\n let originalVariantPrice: number | null = null\n\n if (cheapest.originalPrice && cheapest.originalPrice > cheapest.price) {\n originalVariantPrice = cheapest.originalPrice\n } else if (cheapest.discount && cheapest.discount.difference > 0) {\n originalVariantPrice = cheapest.price + cheapest.discount.difference\n }\n\n if (!originalVariantPrice) return null\n\n return originalVariantPrice + minRequiredModifiersCost.value\n })\n\n const hasDifferentPrices = computed<boolean>(() => {\n if (resolvedPrices.value.length <= 1) {\n return false\n }\n\n const prices = new Set(resolvedPrices.value.map((r) => r.price))\n\n return prices.size > 1\n })\n\n const needsSelection = computed<boolean>(() => {\n if (variants.value.length > 1) {\n return true\n }\n\n const mods = product.value.modifiers\n const groups = product.value.modifiers_groups\n\n return (\n (Array.isArray(mods) && mods.length > 0) || (Array.isArray(groups) && groups.length > 0)\n )\n })\n\n return { displayPrice, originalDisplayPrice, hasDifferentPrices, needsSelection }\n}\n"]}
|
|
@@ -1,48 +1,11 @@
|
|
|
1
|
+
import { resolveVariantPrice } from './chunk-6HOKVW4M.js';
|
|
1
2
|
import { createDebounce } from './chunk-XUGXLJZB.js';
|
|
2
3
|
import { createRuntimeState } from './chunk-5QCUH3FF.js';
|
|
3
4
|
import { useCatalogStore } from './chunk-4JCJHAL3.js';
|
|
4
|
-
import { useRuntime, useAuthStore, useCartStore } from './chunk-
|
|
5
|
+
import { useRuntime, useAuthStore, useCartStore } from './chunk-TXR76IDL.js';
|
|
5
6
|
import { useStoreProfileStore } from './chunk-IOYYCA75.js';
|
|
6
|
-
import { UnitType } from '@zenky/storefront-api';
|
|
7
7
|
import { computed, ref, watch } from 'vue';
|
|
8
8
|
|
|
9
|
-
var UNIT_LABELS = {
|
|
10
|
-
[UnitType.Gram]: "\u0433",
|
|
11
|
-
[UnitType.Kilogram]: "\u043A\u0433",
|
|
12
|
-
[UnitType.Meter]: "\u043C",
|
|
13
|
-
[UnitType.Piece]: "\u0433"
|
|
14
|
-
};
|
|
15
|
-
function getWeightFromDimensions(variants) {
|
|
16
|
-
if (!variants?.length) return null;
|
|
17
|
-
const dimension = variants[0]?.dimensions?.find((d) => d.dimension === "weight");
|
|
18
|
-
return dimension?.value ?? null;
|
|
19
|
-
}
|
|
20
|
-
function getVariantWeight(variant) {
|
|
21
|
-
if (!variant) return null;
|
|
22
|
-
return variant.dimensions?.find((d) => d.dimension === "weight")?.value ?? null;
|
|
23
|
-
}
|
|
24
|
-
function getProductWeight(product) {
|
|
25
|
-
return product.weight ?? getWeightFromDimensions(product.variants) ?? null;
|
|
26
|
-
}
|
|
27
|
-
function toGrams(weight, unitType) {
|
|
28
|
-
if (!weight) {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
if (unitType === UnitType.Kilogram) {
|
|
32
|
-
return weight * 1e3;
|
|
33
|
-
}
|
|
34
|
-
if (unitType === UnitType.Meter) {
|
|
35
|
-
return null;
|
|
36
|
-
}
|
|
37
|
-
return weight;
|
|
38
|
-
}
|
|
39
|
-
function formatWeight(weight, unitType) {
|
|
40
|
-
if (!weight) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
const label = UNIT_LABELS[unitType];
|
|
44
|
-
return `${weight} ${label}`;
|
|
45
|
-
}
|
|
46
9
|
function useProductsByCategory(products, categories) {
|
|
47
10
|
const grouped = computed(() => {
|
|
48
11
|
if (!categories.value.length || !products.value.length) {
|
|
@@ -105,92 +68,6 @@ function useProductVariantModifiers(variant) {
|
|
|
105
68
|
});
|
|
106
69
|
return { modifiersModel, selectedModifiersHash, hasAnyModifier };
|
|
107
70
|
}
|
|
108
|
-
function resolveVariantPrice(variant, stockId) {
|
|
109
|
-
if (variant.prices?.length) {
|
|
110
|
-
if (stockId) {
|
|
111
|
-
const stockEntry = variant.prices.find((p) => p.stock_id === stockId);
|
|
112
|
-
if (stockEntry) {
|
|
113
|
-
return {
|
|
114
|
-
price: stockEntry.price,
|
|
115
|
-
originalPrice: stockEntry.original_price,
|
|
116
|
-
discount: stockEntry.discount
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
const globalEntry = variant.prices.find((p) => p.stock_id === null);
|
|
121
|
-
if (globalEntry) {
|
|
122
|
-
return {
|
|
123
|
-
price: globalEntry.price,
|
|
124
|
-
originalPrice: globalEntry.original_price,
|
|
125
|
-
discount: globalEntry.discount
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return {
|
|
130
|
-
price: variant.price,
|
|
131
|
-
originalPrice: variant.original_price ?? null,
|
|
132
|
-
discount: variant.discount ?? null
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
function useProductPricing(product, stockId) {
|
|
136
|
-
const variants = computed(() => product.value.variants ?? []);
|
|
137
|
-
const currentStockId = computed(() => stockId?.value ?? null);
|
|
138
|
-
const resolvedPrices = computed(
|
|
139
|
-
() => variants.value.map((v) => resolveVariantPrice(v, currentStockId.value))
|
|
140
|
-
);
|
|
141
|
-
const minRequiredModifiersCost = computed(() => {
|
|
142
|
-
let total = 0;
|
|
143
|
-
for (const groupEntry of product.value.modifiers_groups ?? []) {
|
|
144
|
-
const group = groupEntry.group;
|
|
145
|
-
for (const modEntry of group.modifiers) {
|
|
146
|
-
if (modEntry.is_required && modEntry.modifier.price) {
|
|
147
|
-
total += modEntry.modifier.price * Math.max(modEntry.min_quantity ?? 0, 1);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
if (groupEntry.is_required && group.modifiers.length && !group.modifiers.some((m) => m.is_required)) {
|
|
151
|
-
const cheapest = group.modifiers.reduce(
|
|
152
|
-
(min, m) => (m.modifier.price ?? 0) < (min.modifier.price ?? 0) ? m : min
|
|
153
|
-
);
|
|
154
|
-
total += (cheapest.modifier.price ?? 0) * Math.max(cheapest.min_quantity ?? 0, 1);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
return total;
|
|
158
|
-
});
|
|
159
|
-
const displayPrice = computed(() => {
|
|
160
|
-
if (!resolvedPrices.value.length) {
|
|
161
|
-
return 0;
|
|
162
|
-
}
|
|
163
|
-
return Math.min(...resolvedPrices.value.map((r) => r.price)) + minRequiredModifiersCost.value;
|
|
164
|
-
});
|
|
165
|
-
const originalDisplayPrice = computed(() => {
|
|
166
|
-
if (!resolvedPrices.value.length) return null;
|
|
167
|
-
const cheapest = resolvedPrices.value.reduce((min, r) => r.price < min.price ? r : min);
|
|
168
|
-
let originalVariantPrice = null;
|
|
169
|
-
if (cheapest.originalPrice && cheapest.originalPrice > cheapest.price) {
|
|
170
|
-
originalVariantPrice = cheapest.originalPrice;
|
|
171
|
-
} else if (cheapest.discount && cheapest.discount.difference > 0) {
|
|
172
|
-
originalVariantPrice = cheapest.price + cheapest.discount.difference;
|
|
173
|
-
}
|
|
174
|
-
if (!originalVariantPrice) return null;
|
|
175
|
-
return originalVariantPrice + minRequiredModifiersCost.value;
|
|
176
|
-
});
|
|
177
|
-
const hasDifferentPrices = computed(() => {
|
|
178
|
-
if (resolvedPrices.value.length <= 1) {
|
|
179
|
-
return false;
|
|
180
|
-
}
|
|
181
|
-
const prices = new Set(resolvedPrices.value.map((r) => r.price));
|
|
182
|
-
return prices.size > 1;
|
|
183
|
-
});
|
|
184
|
-
const needsSelection = computed(() => {
|
|
185
|
-
if (variants.value.length > 1) {
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
const mods = product.value.modifiers;
|
|
189
|
-
const groups = product.value.modifiers_groups;
|
|
190
|
-
return Array.isArray(mods) && mods.length > 0 || Array.isArray(groups) && groups.length > 0;
|
|
191
|
-
});
|
|
192
|
-
return { displayPrice, originalDisplayPrice, hasDifferentPrices, needsSelection };
|
|
193
|
-
}
|
|
194
71
|
function getDefaultOptionsModel(product) {
|
|
195
72
|
const model = {};
|
|
196
73
|
if (!product.variants?.length) {
|
|
@@ -629,6 +506,164 @@ function useProductList() {
|
|
|
629
506
|
return { fetchProducts };
|
|
630
507
|
}
|
|
631
508
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
509
|
+
// src/utils/color.ts
|
|
510
|
+
var WHITE = "#ffffff";
|
|
511
|
+
var BLACK = "#000000";
|
|
512
|
+
var CONTRAST_STEPS = 14;
|
|
513
|
+
var CONTRAST_STEP_WEIGHT = 0.1;
|
|
514
|
+
function parseHex(hex) {
|
|
515
|
+
const digits = hex.slice(1);
|
|
516
|
+
const full = digits.length <= 4 ? digits.split("").map((char) => char + char).join("") : digits;
|
|
517
|
+
return [0, 2, 4].map((offset) => parseInt(full.slice(offset, offset + 2), 16));
|
|
518
|
+
}
|
|
519
|
+
function toHex([r, g, b]) {
|
|
520
|
+
return `#${[r, g, b].map(
|
|
521
|
+
(channel) => Math.round(Math.max(0, Math.min(255, channel))).toString(16).padStart(2, "0")
|
|
522
|
+
).join("")}`;
|
|
523
|
+
}
|
|
524
|
+
function luminance(hex) {
|
|
525
|
+
const [r, g, b] = parseHex(hex).map((channel) => {
|
|
526
|
+
const value = channel / 255;
|
|
527
|
+
return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
528
|
+
});
|
|
529
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
|
530
|
+
}
|
|
531
|
+
function contrastRatio(a, b) {
|
|
532
|
+
const first = luminance(a);
|
|
533
|
+
const second = luminance(b);
|
|
534
|
+
return (Math.max(first, second) + 0.05) / (Math.min(first, second) + 0.05);
|
|
535
|
+
}
|
|
536
|
+
function chroma(hex) {
|
|
537
|
+
const channels = parseHex(hex).map((channel) => channel / 255);
|
|
538
|
+
return Math.max(...channels) - Math.min(...channels);
|
|
539
|
+
}
|
|
540
|
+
function mixColors(from, to, weightOfTo) {
|
|
541
|
+
const a = parseHex(from);
|
|
542
|
+
const b = parseHex(to);
|
|
543
|
+
return toHex(a.map((channel, index) => channel + (b[index] - channel) * weightOfTo));
|
|
544
|
+
}
|
|
545
|
+
function toRgba(hex, alpha) {
|
|
546
|
+
const [r, g, b] = parseHex(hex);
|
|
547
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
|
548
|
+
}
|
|
549
|
+
function rotateHue(hex, degrees, { lightnessBoost = 0, maxLightness = 1 } = {}) {
|
|
550
|
+
const [r, g, b] = parseHex(hex).map((channel2) => channel2 / 255);
|
|
551
|
+
const max = Math.max(r, g, b);
|
|
552
|
+
const min = Math.min(r, g, b);
|
|
553
|
+
const delta = max - min;
|
|
554
|
+
const lightness = (max + min) / 2;
|
|
555
|
+
let hue = 0;
|
|
556
|
+
let saturation = 0;
|
|
557
|
+
if (delta) {
|
|
558
|
+
saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);
|
|
559
|
+
if (max === r) hue = (g - b) / delta + (g < b ? 6 : 0);
|
|
560
|
+
else if (max === g) hue = (b - r) / delta + 2;
|
|
561
|
+
else hue = (r - g) / delta + 4;
|
|
562
|
+
hue *= 60;
|
|
563
|
+
}
|
|
564
|
+
const rotatedHue = ((hue + degrees) % 360 + 360) % 360;
|
|
565
|
+
const rotatedLightness = Math.min(lightness + lightnessBoost, maxLightness);
|
|
566
|
+
const spread = (1 - Math.abs(2 * rotatedLightness - 1)) * saturation / 2;
|
|
567
|
+
const channel = (offset) => {
|
|
568
|
+
const position = (offset + rotatedHue / 30) % 12;
|
|
569
|
+
const value = rotatedLightness - spread * Math.max(-1, Math.min(position - 3, 9 - position, 1));
|
|
570
|
+
return value * 255;
|
|
571
|
+
};
|
|
572
|
+
return toHex([channel(0), channel(8), channel(4)]);
|
|
573
|
+
}
|
|
574
|
+
function ensureContrast(color, against, minimum) {
|
|
575
|
+
const target = luminance(against) > 0.5 ? BLACK : WHITE;
|
|
576
|
+
let result = color;
|
|
577
|
+
for (let step = 0; step < CONTRAST_STEPS && contrastRatio(result, against) < minimum; step++) {
|
|
578
|
+
result = mixColors(result, target, CONTRAST_STEP_WEIGHT);
|
|
579
|
+
}
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// src/products/badges.ts
|
|
584
|
+
var ON_ACCENT_CONTRAST_MIN = 3;
|
|
585
|
+
var TEXT_CONTRAST_MIN = 4.5;
|
|
586
|
+
var NEUTRAL_CHROMA_GAP = 0.08;
|
|
587
|
+
var HUE_SHIFT = 38;
|
|
588
|
+
var HUE_SHIFT_LIGHTNESS_BOOST = 0.06;
|
|
589
|
+
var HUE_SHIFT_MAX_LIGHTNESS = 0.7;
|
|
590
|
+
var GLASS_ALPHA = 0.35;
|
|
591
|
+
var HEX_COLOR = /^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i;
|
|
592
|
+
var HTTP_URL = /^https?:\/\//i;
|
|
593
|
+
function cleanText(value) {
|
|
594
|
+
if (typeof value !== "string") return null;
|
|
595
|
+
const text = value.trim();
|
|
596
|
+
return text || null;
|
|
597
|
+
}
|
|
598
|
+
function dropAlpha(hex) {
|
|
599
|
+
if (hex.length === 5) return hex.slice(0, 4);
|
|
600
|
+
if (hex.length === 9) return hex.slice(0, 7);
|
|
601
|
+
return hex;
|
|
602
|
+
}
|
|
603
|
+
function toColors(badge) {
|
|
604
|
+
const background = cleanText(badge.colors?.background);
|
|
605
|
+
const text = cleanText(badge.colors?.text);
|
|
606
|
+
if (!background || !text || !HEX_COLOR.test(background) || !HEX_COLOR.test(text)) {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
return { background: dropAlpha(background), text: dropAlpha(text) };
|
|
610
|
+
}
|
|
611
|
+
function toView(badge) {
|
|
612
|
+
const label = cleanText(badge.label);
|
|
613
|
+
const iconValue = cleanText(badge.icon?.value);
|
|
614
|
+
const emoji = badge.icon?.type === "emoji" ? iconValue : null;
|
|
615
|
+
const imageUrl = badge.icon?.type === "image" && iconValue && HTTP_URL.test(iconValue) ? iconValue : null;
|
|
616
|
+
if (!label && !emoji && !imageUrl) return null;
|
|
617
|
+
return { id: badge.id, label, emoji, imageUrl, colors: toColors(badge) };
|
|
618
|
+
}
|
|
619
|
+
function toProductBadgeViews(badges) {
|
|
620
|
+
if (!Array.isArray(badges)) return [];
|
|
621
|
+
const result = [];
|
|
622
|
+
const seen = /* @__PURE__ */ new Set();
|
|
623
|
+
for (const badge of badges) {
|
|
624
|
+
if (!badge || typeof badge.id !== "string" || !badge.id || seen.has(badge.id)) continue;
|
|
625
|
+
const view = toView(badge);
|
|
626
|
+
if (!view) continue;
|
|
627
|
+
seen.add(badge.id);
|
|
628
|
+
result.push(view);
|
|
629
|
+
}
|
|
630
|
+
return result;
|
|
631
|
+
}
|
|
632
|
+
function getProductBadgeLayout(badge) {
|
|
633
|
+
return !badge.label && badge.imageUrl ? "image-square" : "text";
|
|
634
|
+
}
|
|
635
|
+
function pickAccent(colors) {
|
|
636
|
+
const textChroma = chroma(colors.text);
|
|
637
|
+
const backgroundChroma = chroma(colors.background);
|
|
638
|
+
if (Math.abs(textChroma - backgroundChroma) > NEUTRAL_CHROMA_GAP) {
|
|
639
|
+
return textChroma > backgroundChroma ? colors.text : colors.background;
|
|
640
|
+
}
|
|
641
|
+
return luminance(colors.text) <= luminance(colors.background) ? colors.text : colors.background;
|
|
642
|
+
}
|
|
643
|
+
function getProductBadgeAccent(colors) {
|
|
644
|
+
const base = pickAccent(colors);
|
|
645
|
+
const on = contrastRatio(WHITE, base) >= ON_ACCENT_CONTRAST_MIN ? WHITE : BLACK;
|
|
646
|
+
return {
|
|
647
|
+
base,
|
|
648
|
+
shifted: ensureContrast(
|
|
649
|
+
rotateHue(base, HUE_SHIFT, {
|
|
650
|
+
lightnessBoost: HUE_SHIFT_LIGHTNESS_BOOST,
|
|
651
|
+
maxLightness: HUE_SHIFT_MAX_LIGHTNESS
|
|
652
|
+
}),
|
|
653
|
+
on,
|
|
654
|
+
ON_ACCENT_CONTRAST_MIN
|
|
655
|
+
),
|
|
656
|
+
on,
|
|
657
|
+
text: ensureContrast(base, WHITE, TEXT_CONTRAST_MIN),
|
|
658
|
+
glass: toRgba(base, GLASS_ALPHA)
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
function filterProductBadges(badges, visibility) {
|
|
662
|
+
return badges.filter(
|
|
663
|
+
(badge) => getProductBadgeLayout(badge) === "image-square" ? visibility.showImages : visibility.showText
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export { canAddDirectly, filterProductBadges, findProductInCatalog, getProductBadgeAccent, getProductBadgeLayout, toProductBadgeViews, usePriceRecalculation, useProductDetail, useProductList, useProductVariant, useProductVariantModifiers, useProductVariantSelector, useProductsByCategory, useRelatedProducts };
|
|
668
|
+
//# sourceMappingURL=chunk-CQDOEP6T.js.map
|
|
669
|
+
//# sourceMappingURL=chunk-CQDOEP6T.js.map
|