@osimart/create-storefront 0.0.0-stage → 1.0.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/README.md +80 -2
- package/THEME_DEV_API.md +80 -0
- package/app/app.vue +154 -0
- package/app/assets/css/osi-controls.css +824 -0
- package/app/components/SectionRenderer.vue +46 -0
- package/app/components/builder/BuilderOverlay.vue +582 -0
- package/app/components/checkout/ExtraFees.vue +329 -0
- package/app/components/checkout/PaymentHandoff.vue +388 -0
- package/app/components/common/Banner.vue +35 -0
- package/app/components/common/ConfirmDeleteModal.vue +117 -0
- package/app/components/common/DropdownSelect.vue +432 -0
- package/app/components/common/Loader.vue +32 -0
- package/app/components/common/OsiField.vue +218 -0
- package/app/components/common/PhoneNumInput.vue +434 -0
- package/app/composables/UseBodyScrollLock.js +122 -0
- package/app/composables/UseThemeGroup.js +575 -0
- package/app/composables/useAddToCart.js +87 -0
- package/app/composables/useAnalytics.js +15 -0
- package/app/composables/useAnnouncements.js +29 -0
- package/app/composables/useApiErrorToast.js +104 -0
- package/app/composables/useAuth.js +99 -0
- package/app/composables/useBanners.js +25 -0
- package/app/composables/useBuilderPreview.js +320 -0
- package/app/composables/useButtonStyle.js +107 -0
- package/app/composables/useCarousel.js +160 -0
- package/app/composables/useCart.js +109 -0
- package/app/composables/useCheckout.js +76 -0
- package/app/composables/useClassificationItems.js +269 -0
- package/app/composables/useClassificationPage.js +545 -0
- package/app/composables/useClassificationUrl.js +63 -0
- package/app/composables/useColorScheme.js +44 -0
- package/app/composables/useContactForm.js +53 -0
- package/app/composables/useContentTree.js +770 -0
- package/app/composables/useDragScroll.js +73 -0
- package/app/composables/useFooterSocials.js +107 -0
- package/app/composables/useInputStyle.js +45 -0
- package/app/composables/useLayoutShell.js +114 -0
- package/app/composables/useLazyVideo.js +36 -0
- package/app/composables/useMediaTypes.js +66 -0
- package/app/composables/useMediaUrl.js +71 -0
- package/app/composables/useMenu.js +100 -0
- package/app/composables/useNavStyle.js +219 -0
- package/app/composables/usePlaceholders.js +294 -0
- package/app/composables/usePolicies.js +26 -0
- package/app/composables/useProductCardMedia.js +135 -0
- package/app/composables/useProductCardStyle.js +132 -0
- package/app/composables/useProductDetail.js +982 -0
- package/app/composables/useProductGallery.js +166 -0
- package/app/composables/useProductItems.js +270 -0
- package/app/composables/useProductUrl.js +60 -0
- package/app/composables/useProductVariants.js +210 -0
- package/app/composables/useQuickShop.js +115 -0
- package/app/composables/useRelatedProducts.js +186 -0
- package/app/composables/useRenderContext.js +45 -0
- package/app/composables/useScopedCss.js +106 -0
- package/app/composables/useSearch.js +35 -0
- package/app/composables/useSectionData.js +462 -0
- package/app/composables/useSectionShell.js +113 -0
- package/app/composables/useSidebar.js +69 -0
- package/app/composables/useStoreData.js +57 -0
- package/app/composables/useStoreInit.js +52 -0
- package/app/composables/useStorefront.js +239 -0
- package/app/composables/useT.js +21 -0
- package/app/composables/useTextStyles.js +84 -0
- package/app/composables/useThemeControls.js +34 -0
- package/app/composables/useThemeSeo.js +91 -0
- package/app/composables/useThemeTokens.js +171 -0
- package/app/composables/useWishlist.js +66 -0
- package/app/logic/components/AnnouncementBar.js +118 -0
- package/app/logic/components/CardGrid.js +125 -0
- package/app/logic/components/CardList.js +98 -0
- package/app/logic/components/CartDrawer.js +273 -0
- package/app/logic/components/CartItem.js +31 -0
- package/app/logic/components/ClassificationBento.js +6 -0
- package/app/logic/components/ClassificationCard.js +6 -0
- package/app/logic/components/ClassificationCarousel.js +115 -0
- package/app/logic/components/ClassificationEditorial.js +41 -0
- package/app/logic/components/ClassificationFeatured.js +57 -0
- package/app/logic/components/ClassificationGrid.js +6 -0
- package/app/logic/components/ClassificationNav.js +61 -0
- package/app/logic/components/ClassificationPart.js +72 -0
- package/app/logic/components/FooterBlock.js +91 -0
- package/app/logic/components/FooterSocials.js +60 -0
- package/app/logic/components/HeaderDropdownList.js +29 -0
- package/app/logic/components/HeaderIcon.js +68 -0
- package/app/logic/components/HeaderLogo.js +39 -0
- package/app/logic/components/HeaderNav.js +268 -0
- package/app/logic/components/MobileMenu.js +280 -0
- package/app/logic/components/ProductBuyButtons.js +66 -0
- package/app/logic/components/ProductCard.js +160 -0
- package/app/logic/components/ProductFilters.js +37 -0
- package/app/logic/components/ProductGallery.js +19 -0
- package/app/logic/components/ProductInfoBlock.js +411 -0
- package/app/logic/components/ProductLightbox.js +12 -0
- package/app/logic/components/ProductQuantity.js +31 -0
- package/app/logic/components/ProductVariantPicker.js +125 -0
- package/app/logic/components/QuickBuy.js +194 -0
- package/app/logic/components/SearchOverlay.js +41 -0
- package/app/logic/components/SidebarMenuItem.js +39 -0
- package/app/logic/components/SiteFooter.js +476 -0
- package/app/logic/components/SiteHeader.js +266 -0
- package/app/logic/components/WhatsAppButton.js +75 -0
- package/app/logic/layouts/default.js +66 -0
- package/app/logic/sections/AnnouncementBarSection.js +6 -0
- package/app/logic/sections/BlockCarousel.js +103 -0
- package/app/logic/sections/ClassificationListSection.js +40 -0
- package/app/logic/sections/ContactFormSection.js +294 -0
- package/app/logic/sections/ContactInfoSection.js +419 -0
- package/app/logic/sections/ContentNode.js +189 -0
- package/app/logic/sections/CountdownSection.js +254 -0
- package/app/logic/sections/CustomHtmlSection.js +51 -0
- package/app/logic/sections/DividerSection.js +31 -0
- package/app/logic/sections/FaqSection.js +53 -0
- package/app/logic/sections/FeatureListSection.js +6 -0
- package/app/logic/sections/FeaturedProductSection.js +40 -0
- package/app/logic/sections/FooterSection.js +16 -0
- package/app/logic/sections/FooterUtilitiesSection.js +24 -0
- package/app/logic/sections/GallerySection.js +232 -0
- package/app/logic/sections/HeaderSection.js +24 -0
- package/app/logic/sections/HeroSection.js +24 -0
- package/app/logic/sections/LogoListSection.js +6 -0
- package/app/logic/sections/MainClassificationSection.js +473 -0
- package/app/logic/sections/MainProductSection.js +211 -0
- package/app/logic/sections/MainSearchSection.js +6 -0
- package/app/logic/sections/MapSection.js +92 -0
- package/app/logic/sections/MediaContentSection.js +38 -0
- package/app/logic/sections/MediaWithTextSection.js +238 -0
- package/app/logic/sections/NewsletterSection.js +126 -0
- package/app/logic/sections/ProductHighlightSection.js +201 -0
- package/app/logic/sections/ProductListSection.js +45 -0
- package/app/logic/sections/ProductRecommendationsSection.js +113 -0
- package/app/logic/sections/RichTextSection.js +6 -0
- package/app/logic/sections/ScrollingTextSection.js +69 -0
- package/app/logic/sections/SearchHeaderSection.js +22 -0
- package/app/logic/sections/SlideshowSection.js +118 -0
- package/app/logic/sections/SpacerSection.js +37 -0
- package/app/logic/sections/SplitShowcaseSection.js +6 -0
- package/app/logic/sections/TestimonialListSection.js +6 -0
- package/app/logic/sections/VideoSection.js +235 -0
- package/app/pages/[category]/[product].vue +196 -0
- package/app/pages/__builder-preview.vue +38 -0
- package/app/pages/categories/[slug].vue +208 -0
- package/app/pages/checkout.vue +1972 -0
- package/app/pages/forgot-password.vue +115 -0
- package/app/pages/index.vue +64 -0
- package/app/pages/login.vue +585 -0
- package/app/pages/order-detail.vue +442 -0
- package/app/pages/pages/[handle].vue +58 -0
- package/app/pages/privacy-policy.vue +46 -0
- package/app/pages/resetpassword.vue +138 -0
- package/app/pages/resetverificationcode.vue +163 -0
- package/app/pages/result.vue +203 -0
- package/app/pages/return-policy.vue +50 -0
- package/app/pages/search.vue +100 -0
- package/app/pages/shipping-policy.vue +46 -0
- package/app/pages/user-profile/components/AddAddressModal.vue +452 -0
- package/app/pages/user-profile/components/Whishlist.vue +137 -0
- package/app/pages/user-profile/components/left-bar.vue +233 -0
- package/app/pages/user-profile/components/orders.vue +162 -0
- package/app/pages/user-profile/components/profile-details.vue +566 -0
- package/app/pages/user-profile/components/settings.vue +194 -0
- package/app/pages/user-profile/index.vue +252 -0
- package/app/pages/verificationcode.vue +188 -0
- package/app/plugins/analytics.client.js +99 -0
- package/app/plugins/geo.ts +24 -0
- package/app/plugins/reveal.client.js +53 -0
- package/app/plugins/toast.js +10 -0
- package/app/plugins/vue-query.ts +5 -0
- package/app/services/ApiServices.js +74 -0
- package/app/services/AuthBus.js +162 -0
- package/app/services/Authentication/AuthenticationService.js +133 -0
- package/app/services/Authentication/TokenService.js +62 -0
- package/app/services/CartServices.js +91 -0
- package/app/services/CartToken.js +26 -0
- package/app/services/CategoryServices.js +54 -0
- package/app/services/CheckoutServices.js +113 -0
- package/app/services/HomeServices.js +133 -0
- package/app/services/NewsletterServices.js +29 -0
- package/app/services/PaymentServices.js +71 -0
- package/app/services/PoliciesServices.js +16 -0
- package/app/services/ProductServices.js +79 -0
- package/app/services/SearchServices.js +18 -0
- package/app/services/UserServices.js +105 -0
- package/app/services/UtilsServices.js +46 -0
- package/app/services/WishlistServices.js +51 -0
- package/app/services/storefront.service.js +57 -0
- package/app/stores/userStore.js +13 -0
- package/app/theme/categoryTree.js +68 -0
- package/app/theme/controls.js +88 -0
- package/app/theme/registry.js +56 -0
- package/app/utils/analytics.js +864 -0
- package/app/utils/device.js +50 -0
- package/app/utils/eventBus.js +24 -0
- package/app/utils/icon-presets.js +24 -0
- package/app/utils/productOffers.js +118 -0
- package/app/utils/requestCache.js +36 -0
- package/app/utils/scrollToError.js +59 -0
- package/app/utils/toPlainError.js +30 -0
- package/bin/osimart-theme.mjs +259 -0
- package/nuxt/osi-sections.ts +56 -0
- package/nuxt.config.ts +60 -0
- package/package.json +43 -3
- package/public/css/input.css +29 -0
- package/public/css/output.css +2716 -0
- package/public/favicon-32x32.png +0 -0
- package/public/robots.txt +2 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import CartServices from "#osi/services/CartServices";
|
|
2
|
+
import ProductServices from "#osi/services/ProductServices";
|
|
3
|
+
|
|
4
|
+
export default function useLogic() {
|
|
5
|
+
/**
|
|
6
|
+
* Cart drawer — Shopify-style, driven by the theme's Cart settings
|
|
7
|
+
* (CART_DRAWER_*): title and count, width, colour scheme, empty state,
|
|
8
|
+
* free-shipping progress, product recommendations, subtotal note, checkout
|
|
9
|
+
* label and a "Continue shopping" link.
|
|
10
|
+
*
|
|
11
|
+
* Teleported into #osi-teleport-root (inside .osi-root), not <body>: the
|
|
12
|
+
* theme's button / field styles and colours only apply inside it.
|
|
13
|
+
*/
|
|
14
|
+
const settings = useThemeGroup("CART");
|
|
15
|
+
const t = useT();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Texts fall back to their default when left empty — clearing a field brings
|
|
19
|
+
* the default back instead of leaving a blank heading or button.
|
|
20
|
+
*/
|
|
21
|
+
const TEXT_DEFAULTS = {
|
|
22
|
+
CART_DRAWER_TITLE: "Your cart",
|
|
23
|
+
CART_DRAWER_EMPTY_TITLE: "Your cart is empty",
|
|
24
|
+
CART_DRAWER_EMPTY_TEXT: "Have a look around — we think you will find something you love.",
|
|
25
|
+
CART_DRAWER_EMPTY_BUTTON: "Continue shopping",
|
|
26
|
+
CART_DRAWER_FREE_SHIPPING_MESSAGE: "You're {amount} away from free shipping",
|
|
27
|
+
CART_DRAWER_FREE_SHIPPING_SUCCESS: "You've got free shipping!",
|
|
28
|
+
CART_DRAWER_RECOMMENDATIONS_TITLE: "You may also like",
|
|
29
|
+
CART_DRAWER_SUBTOTAL_NOTE: "Taxes and shipping calculated at checkout.",
|
|
30
|
+
CART_DRAWER_CHECKOUT_LABEL: "Check out",
|
|
31
|
+
CART_DRAWER_RECS_ADD_LABEL: "Add",
|
|
32
|
+
};
|
|
33
|
+
const txt = (key) => String(t(settings.value[key]) ?? "").trim() || TEXT_DEFAULTS[key] || "";
|
|
34
|
+
const scheme = useColorScheme(computed(() => settings.value.CART_DRAWER_COLOR_SCHEME || null));
|
|
35
|
+
const { isCartSidebarVisible, toggleCartSidebar } = useSidebar();
|
|
36
|
+
const { cartItems, cartTotal, count, fetchCart } = useCart();
|
|
37
|
+
const { showApiError } = useApiErrorToast();
|
|
38
|
+
const { mediaUrl } = useMediaUrl();
|
|
39
|
+
const { productUrl } = useProductUrl();
|
|
40
|
+
const { handleAddToCart } = useAddToCart();
|
|
41
|
+
const { store } = useStorefront();
|
|
42
|
+
|
|
43
|
+
const currency = computed(
|
|
44
|
+
() => store.value?.default_currency?.symbol || store.value?.default_currency?.code || "$"
|
|
45
|
+
);
|
|
46
|
+
const money = (n) => `${currency.value}${Number(n || 0).toFixed(2).replace(/\.00$/, "")}`;
|
|
47
|
+
|
|
48
|
+
const px = (v, d) => `${Number.isFinite(Number(v)) && v !== "" && v != null ? Number(v) : d}px`;
|
|
49
|
+
const has = (v) => v !== "" && v != null;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A button's look from its CART_DRAWER_<name>_* settings. Nothing set: the
|
|
53
|
+
* theme's button (Theme settings → Buttons). A style picks the theme's
|
|
54
|
+
* variant; colours / radius override just those bits (--btn-* vars that
|
|
55
|
+
* .osi-btn reads).
|
|
56
|
+
*/
|
|
57
|
+
const VARIANTS = {
|
|
58
|
+
SOLID: "osi-btn--solid",
|
|
59
|
+
OUTLINE: "osi-btn--outline",
|
|
60
|
+
SECONDARY: "osi-btn--secondary",
|
|
61
|
+
UNDERLINE: "osi-btn--underline",
|
|
62
|
+
};
|
|
63
|
+
function buttonVars(prefix) {
|
|
64
|
+
const s = settings.value;
|
|
65
|
+
const style = {};
|
|
66
|
+
if (s[`${prefix}_BG`]) style["--btn-bg"] = s[`${prefix}_BG`];
|
|
67
|
+
if (s[`${prefix}_TEXT`]) style["--btn-fg"] = s[`${prefix}_TEXT`];
|
|
68
|
+
if (s[`${prefix}_BORDER`]) style["--btn-border-color"] = s[`${prefix}_BORDER`];
|
|
69
|
+
if (s[`${prefix}_HOVER_BG`]) style["--btn-hover-bg"] = s[`${prefix}_HOVER_BG`];
|
|
70
|
+
if (s[`${prefix}_HOVER_TEXT`]) style["--btn-hover-fg"] = s[`${prefix}_HOVER_TEXT`];
|
|
71
|
+
if (has(s[`${prefix}_RADIUS`])) style["--btn-radius"] = px(s[`${prefix}_RADIUS`], 0);
|
|
72
|
+
return style;
|
|
73
|
+
}
|
|
74
|
+
function button(prefix) {
|
|
75
|
+
return {
|
|
76
|
+
class: VARIANTS[String(settings.value[`${prefix}_STYLE`] || "").toUpperCase()] ?? "",
|
|
77
|
+
style: buttonVars(prefix),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const checkoutBtn = computed(() => button("CART_DRAWER_CHECKOUT"));
|
|
81
|
+
const emptyBtn = computed(() => button("CART_DRAWER_EMPTY_BTN"));
|
|
82
|
+
const addBtn = computed(() => button("CART_DRAWER_RECS_ADD"));
|
|
83
|
+
|
|
84
|
+
/** Recommendations' "between products": nothing, a border, or a line. */
|
|
85
|
+
const cardStyle = computed(() => {
|
|
86
|
+
const s = settings.value;
|
|
87
|
+
// Older setting: a border on / off.
|
|
88
|
+
const v = s.CART_DRAWER_RECS_CARD_STYLE || (s.CART_DRAWER_RECS_CARD_BORDER ? "BORDER" : "NONE");
|
|
89
|
+
return String(v).toLowerCase();
|
|
90
|
+
});
|
|
91
|
+
const RATIOS = { SQUARE: "1 / 1", PORTRAIT: "3 / 4", LANDSCAPE: "4 / 3" };
|
|
92
|
+
|
|
93
|
+
const rootStyle = computed(() => {
|
|
94
|
+
const s = settings.value;
|
|
95
|
+
const style = {
|
|
96
|
+
...scheme.value.style,
|
|
97
|
+
"--cart-w": `${Number(s.CART_DRAWER_WIDTH) || 440}px`,
|
|
98
|
+
// Recommendations
|
|
99
|
+
"--recs-cols": String(Math.max(1, Math.min(3, Number(s.CART_DRAWER_RECS_COLUMNS) || 2))),
|
|
100
|
+
"--recs-gap": px(s.CART_DRAWER_RECS_GAP, 12),
|
|
101
|
+
"--recs-ratio": RATIOS[s.CART_DRAWER_RECS_IMAGE_RATIO] ?? RATIOS.PORTRAIT,
|
|
102
|
+
"--recs-img": px(s.CART_DRAWER_RECS_IMAGE_SIZE, 72),
|
|
103
|
+
"--recs-img-r": px(s.CART_DRAWER_RECS_IMAGE_RADIUS, 6),
|
|
104
|
+
"--recs-card-r": px(s.CART_DRAWER_RECS_CARD_RADIUS, 8),
|
|
105
|
+
// Each side on its own; the older single padding is the fallback.
|
|
106
|
+
"--recs-card-px": px(s.CART_DRAWER_RECS_CARD_PADDING_X ?? s.CART_DRAWER_RECS_CARD_PADDING, 0),
|
|
107
|
+
"--recs-card-py": px(s.CART_DRAWER_RECS_CARD_PADDING_Y ?? s.CART_DRAWER_RECS_CARD_PADDING, 0),
|
|
108
|
+
"--recs-lines": String(Math.max(1, Number(s.CART_DRAWER_RECS_NAME_LINES) || 2)),
|
|
109
|
+
"--recs-name": px(s.CART_DRAWER_RECS_NAME_SIZE, 13),
|
|
110
|
+
"--recs-price": px(s.CART_DRAWER_RECS_PRICE_SIZE, 13),
|
|
111
|
+
};
|
|
112
|
+
// Own background / text colour over the scheme's.
|
|
113
|
+
if (s.CART_DRAWER_BACKGROUND) style["--osi-bg"] = s.CART_DRAWER_BACKGROUND;
|
|
114
|
+
if (s.CART_DRAWER_TEXT_COLOR) style["--osi-text"] = s.CART_DRAWER_TEXT_COLOR;
|
|
115
|
+
if (s.CART_DRAWER_RECS_CARD_BACKGROUND) style["--recs-card-bg"] = s.CART_DRAWER_RECS_CARD_BACKGROUND;
|
|
116
|
+
if (s.CART_DRAWER_RECS_PRICE_COLOR) style["--recs-price-color"] = s.CART_DRAWER_RECS_PRICE_COLOR;
|
|
117
|
+
if (s.CART_DRAWER_RECS_LINE_COLOR) style["--recs-line"] = s.CART_DRAWER_RECS_LINE_COLOR;
|
|
118
|
+
// Quantity buttons (inside each cart line): picked up by .osi-qty-btn below.
|
|
119
|
+
const q = buttonVars("CART_DRAWER_QTY");
|
|
120
|
+
for (const [k, v] of Object.entries(q)) style[k.replace("--btn-", "--qty-")] = v;
|
|
121
|
+
if (has(s.CART_DRAWER_QTY_SIZE)) style["--qty-size"] = px(s.CART_DRAWER_QTY_SIZE, 30);
|
|
122
|
+
return style;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
/* ---------------------------------------------------------------- lines */
|
|
126
|
+
|
|
127
|
+
// Per-line, not a single global flag: one slow line shouldn't disable the
|
|
128
|
+
// controls on every other line in the drawer.
|
|
129
|
+
const updatingIds = ref(new Set());
|
|
130
|
+
const isUpdating = (item) => updatingIds.value.has(item.id);
|
|
131
|
+
|
|
132
|
+
async function edit(item, action) {
|
|
133
|
+
if (isUpdating(item)) return;
|
|
134
|
+
updatingIds.value = new Set(updatingIds.value).add(item.id);
|
|
135
|
+
try {
|
|
136
|
+
await CartServices.EditCart(item.id, item.quantity_step || 1, action);
|
|
137
|
+
await fetchCart();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
showApiError(error, "Couldn't update the cart.");
|
|
140
|
+
} finally {
|
|
141
|
+
const next = new Set(updatingIds.value);
|
|
142
|
+
next.delete(item.id);
|
|
143
|
+
updatingIds.value = next;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const increase = (item) => edit(item, "add");
|
|
147
|
+
const decrease = (item) => edit(item, "remove");
|
|
148
|
+
|
|
149
|
+
function close() {
|
|
150
|
+
toggleCartSidebar(false);
|
|
151
|
+
}
|
|
152
|
+
function go(path) {
|
|
153
|
+
close();
|
|
154
|
+
navigateTo(path || "/");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* -------------------------------------------------------- free shipping */
|
|
158
|
+
|
|
159
|
+
const threshold = computed(() => Number(settings.value.CART_DRAWER_FREE_SHIPPING_THRESHOLD) || 0);
|
|
160
|
+
const shipping = computed(() => {
|
|
161
|
+
if (!settings.value.CART_DRAWER_FREE_SHIPPING || threshold.value <= 0) return null;
|
|
162
|
+
const total = Number(cartTotal.value) || 0;
|
|
163
|
+
const left = Math.max(0, threshold.value - total);
|
|
164
|
+
const done = left <= 0;
|
|
165
|
+
const s = settings.value;
|
|
166
|
+
return {
|
|
167
|
+
percent: Math.min(100, (total / threshold.value) * 100),
|
|
168
|
+
done,
|
|
169
|
+
text: done
|
|
170
|
+
? txt("CART_DRAWER_FREE_SHIPPING_SUCCESS")
|
|
171
|
+
: txt("CART_DRAWER_FREE_SHIPPING_MESSAGE").replace("{amount}", money(left)),
|
|
172
|
+
// Its own colour once reached, if set.
|
|
173
|
+
color: (done && s.CART_DRAWER_PROGRESS_DONE_COLOR) || s.CART_DRAWER_PROGRESS_COLOR || null,
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/* ------------------------------------------------------ recommendations */
|
|
178
|
+
|
|
179
|
+
const recs = ref([]);
|
|
180
|
+
const recsLoading = ref(false);
|
|
181
|
+
let recsKey = "";
|
|
182
|
+
|
|
183
|
+
const pickedIds = computed(() =>
|
|
184
|
+
(settings.value.CART_DRAWER_RECOMMENDATIONS_PRODUCTS ?? [])
|
|
185
|
+
.map((v) => (v && typeof v === "object" ? v.id : v))
|
|
186
|
+
.filter((v) => v != null && v !== "")
|
|
187
|
+
);
|
|
188
|
+
const limit = computed(() =>
|
|
189
|
+
Math.max(1, Math.min(12, Number(settings.value.CART_DRAWER_RECOMMENDATIONS_LIMIT) || 4))
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Picked products (in their order), or — none picked — products from the
|
|
194
|
+
* store, fetched by page size. A few extra, so items already in the cart can
|
|
195
|
+
* be left out.
|
|
196
|
+
*/
|
|
197
|
+
async function loadRecs() {
|
|
198
|
+
if (!settings.value.CART_DRAWER_RECOMMENDATIONS) return;
|
|
199
|
+
const ids = pickedIds.value;
|
|
200
|
+
const key = `${ids.join(",")}|${limit.value}`;
|
|
201
|
+
if (key === recsKey && recs.value.length) return;
|
|
202
|
+
recsKey = key;
|
|
203
|
+
recsLoading.value = true;
|
|
204
|
+
try {
|
|
205
|
+
if (ids.length) {
|
|
206
|
+
const res = await ProductServices.getProductsByIds(ids);
|
|
207
|
+
const rows = res?.results ?? res ?? [];
|
|
208
|
+
const byId = new Map(rows.map((p) => [String(p.id), p]));
|
|
209
|
+
recs.value = ids.map((id) => byId.get(String(id))).filter(Boolean);
|
|
210
|
+
} else {
|
|
211
|
+
const res = await ProductServices.listProducts({ page_size: limit.value + 4 });
|
|
212
|
+
recs.value = res?.results ?? res ?? [];
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
recs.value = [];
|
|
216
|
+
} finally {
|
|
217
|
+
recsLoading.value = false;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const inCart = computed(() => new Set(cartItems.value.map((i) => String(i.product?.id ?? i.product_id ?? ""))));
|
|
222
|
+
const shownRecs = computed(() =>
|
|
223
|
+
recs.value.filter((p) => !inCart.value.has(String(p.id))).slice(0, limit.value)
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const recsLayout = computed(() => String(settings.value.CART_DRAWER_RECS_LAYOUT || "GRID").toLowerCase());
|
|
227
|
+
const pricePos = computed(() => String(settings.value.CART_DRAWER_RECS_PRICE_POSITION || "BELOW").toLowerCase());
|
|
228
|
+
|
|
229
|
+
/** Add from the drawer: products with options open their page instead. */
|
|
230
|
+
const adding = ref(null);
|
|
231
|
+
async function addRec(product) {
|
|
232
|
+
if (adding.value) return;
|
|
233
|
+
adding.value = product.id;
|
|
234
|
+
try {
|
|
235
|
+
await handleAddToCart(product);
|
|
236
|
+
await fetchCart();
|
|
237
|
+
} catch {
|
|
238
|
+
// handleAddToCart shows its own error.
|
|
239
|
+
} finally {
|
|
240
|
+
adding.value = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function priceOf(product) {
|
|
245
|
+
const parts = String(product?.discounted_price_range ?? product?.price_range ?? product?.price ?? "")
|
|
246
|
+
.split("-")
|
|
247
|
+
.map((v) => parseFloat(v))
|
|
248
|
+
.filter((n) => Number.isFinite(n) && n > 0);
|
|
249
|
+
if (!parts.length) return "";
|
|
250
|
+
return parts.length > 1 && parts[0] !== parts[1]
|
|
251
|
+
? `${money(parts[0])} – ${money(parts[1])}`
|
|
252
|
+
: money(parts[0]);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// First open (and after the settings change): load suggestions lazily —
|
|
256
|
+
// most visits never open the cart.
|
|
257
|
+
watch(isCartSidebarVisible, (open) => open && loadRecs());
|
|
258
|
+
|
|
259
|
+
// view_cart: the shopper opened their bag. (Skipped by analytics when the
|
|
260
|
+
// drawer only opened itself because something was just added.)
|
|
261
|
+
const analytics = useAnalytics();
|
|
262
|
+
watch(isCartSidebarVisible, (open) => open && analytics.viewCart(cartItems.value));
|
|
263
|
+
watch(
|
|
264
|
+
() => [settings.value.CART_DRAWER_RECOMMENDATIONS, pickedIds.value.join(","), limit.value],
|
|
265
|
+
() => isCartSidebarVisible.value && loadRecs()
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// The drawer mounts once in the layout and outlives every page, so it can't
|
|
269
|
+
// rely on a page's onMounted to load the cart.
|
|
270
|
+
onMounted(fetchCart);
|
|
271
|
+
|
|
272
|
+
return { settings, t, TEXT_DEFAULTS, txt, scheme, isCartSidebarVisible, toggleCartSidebar, cartItems, cartTotal, count, fetchCart, showApiError, mediaUrl, productUrl, handleAddToCart, store, currency, money, px, has, VARIANTS, buttonVars, button, checkoutBtn, emptyBtn, addBtn, cardStyle, RATIOS, rootStyle, updatingIds, isUpdating, edit, increase, decrease, close, go, threshold, shipping, recs, recsLoading, recsKey, pickedIds, limit, loadRecs, inCart, shownRecs, recsLayout, pricePos, adding, addRec, priceOf, analytics };
|
|
273
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
|
|
2
|
+
export default function useLogic({ props, emit }) {
|
|
3
|
+
const { mediaUrl } = useMediaUrl();
|
|
4
|
+
const { store } = useStorefront();
|
|
5
|
+
|
|
6
|
+
const currency = computed(
|
|
7
|
+
() => store.value?.default_currency?.symbol || store.value?.default_currency?.code || "$"
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
// `item.image` is a bare path on cart lines but a media object elsewhere;
|
|
11
|
+
// mediaUrl() takes either.
|
|
12
|
+
const imageSrc = computed(() => mediaUrl(props.item?.image));
|
|
13
|
+
|
|
14
|
+
const variantLabel = computed(() => {
|
|
15
|
+
const values = props.item?.values;
|
|
16
|
+
if (Array.isArray(values)) return values.length ? values.join(" | ") : "";
|
|
17
|
+
return values || "";
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function handleRemove() {
|
|
21
|
+
if (props.loading) return;
|
|
22
|
+
emit("remove-item", props.item);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function handleAdd() {
|
|
26
|
+
if (props.loading) return;
|
|
27
|
+
emit("add-item", props.item);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return { mediaUrl, store, currency, imageSrc, variantLabel, handleRemove, handleAdd };
|
|
31
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Autoplay, Pagination } from "swiper/modules";
|
|
2
|
+
|
|
3
|
+
export default function useLogic({ props }) {
|
|
4
|
+
const perView = computed(() => Number(props.options.perView) || 4);
|
|
5
|
+
const gap = computed(() => Number(props.options.gap) || 0);
|
|
6
|
+
|
|
7
|
+
// Looping fewer items than fit on screen has nothing to rotate.
|
|
8
|
+
const loop = computed(() => !!props.options.loop && props.items.length > perView.value);
|
|
9
|
+
|
|
10
|
+
const { slides, onSwiper, onSlideChange, goPrev, goNext, isBeginning, isEnd } = useCarousel({
|
|
11
|
+
items: () => props.items,
|
|
12
|
+
loop,
|
|
13
|
+
minLoopSlides: () => perView.value * 3,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const breakpoints = computed(() => ({
|
|
17
|
+
0: {
|
|
18
|
+
slidesPerView: Number(props.options.perViewMobile) || 1.3,
|
|
19
|
+
spaceBetween: Number(props.options.gapMobile ?? Math.min(gap.value, 16)),
|
|
20
|
+
},
|
|
21
|
+
640: { slidesPerView: Number(props.options.perViewTablet) || 2.2, spaceBetween: gap.value },
|
|
22
|
+
1024: { slidesPerView: perView.value, spaceBetween: gap.value },
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const autoplay = computed(() =>
|
|
26
|
+
props.options.autoplay && props.items.length > perView.value
|
|
27
|
+
? {
|
|
28
|
+
delay: (Number(props.options.speed) || 5) * 1000,
|
|
29
|
+
disableOnInteraction: false,
|
|
30
|
+
// Autoplay that keeps moving while someone is reading a card is the
|
|
31
|
+
// single most complained-about carousel behaviour.
|
|
32
|
+
pauseOnMouseEnter: true,
|
|
33
|
+
}
|
|
34
|
+
: false
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
// Arrows and dots for a rail that doesn't scroll are noise.
|
|
38
|
+
const overflows = computed(() => props.items.length > perView.value);
|
|
39
|
+
const arrowPosition = computed(() => String(props.options.arrowPosition || "SIDES").toUpperCase());
|
|
40
|
+
const showArrows = computed(
|
|
41
|
+
() => props.options.showArrows !== false && overflows.value && arrowPosition.value !== "HEADER"
|
|
42
|
+
);
|
|
43
|
+
const showDots = computed(() => !!props.options.showDots && overflows.value);
|
|
44
|
+
const dotsStyle = computed(() => String(props.options.dotsStyle || "DOTS").toUpperCase());
|
|
45
|
+
|
|
46
|
+
// Swiper reads the pagination type (bullets / progressbar / fraction) once, at
|
|
47
|
+
|
|
48
|
+
// init. Changing the prop later updates our CSS classes but leaves the old
|
|
49
|
+
|
|
50
|
+
// pagination element in place, so the preview looked stale until a reload.
|
|
51
|
+
|
|
52
|
+
// Keying the Swiper on these options remounts it whenever they change.
|
|
53
|
+
|
|
54
|
+
const paginationKey = computed(
|
|
55
|
+
|
|
56
|
+
() => `${showDots.value}-${dotsStyle.value}-${!!props.options.dotsDynamic}`
|
|
57
|
+
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
const pagination = computed(() => {
|
|
63
|
+
if (!showDots.value) return false;
|
|
64
|
+
if (dotsStyle.value === "PROGRESS") return { type: "progressbar" };
|
|
65
|
+
if (dotsStyle.value === "FRACTION") return { type: "fraction" };
|
|
66
|
+
return { clickable: true, dynamicBullets: !!props.options.dotsDynamic };
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/* -------------------------------------------------- arrows on the images */
|
|
70
|
+
|
|
71
|
+
const rail = ref(null);
|
|
72
|
+
const arrowTop = ref(null);
|
|
73
|
+
let observer = null;
|
|
74
|
+
|
|
75
|
+
function measureArrowTop() {
|
|
76
|
+
const el = rail.value;
|
|
77
|
+
if (!el) return;
|
|
78
|
+
// The card template's image (or, for an old card, its frame).
|
|
79
|
+
const frame = el.querySelector(".osi-cb-cimg, .osi-cc__frame");
|
|
80
|
+
if (!frame) {
|
|
81
|
+
arrowTop.value = null;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const railBox = el.getBoundingClientRect();
|
|
85
|
+
const box = frame.getBoundingClientRect();
|
|
86
|
+
arrowTop.value = Math.round(box.top - railBox.top + box.height / 2);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
onMounted(() => {
|
|
90
|
+
nextTick(measureArrowTop);
|
|
91
|
+
if (typeof ResizeObserver !== "undefined" && rail.value) {
|
|
92
|
+
observer = new ResizeObserver(measureArrowTop);
|
|
93
|
+
observer.observe(rail.value);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
onBeforeUnmount(() => observer?.disconnect());
|
|
97
|
+
watch(() => props.items.length, () => nextTick(measureArrowTop));
|
|
98
|
+
|
|
99
|
+
const railStyle = computed(() =>
|
|
100
|
+
arrowTop.value != null ? { "--osi-cl-arrow-top": `${arrowTop.value}px` } : {}
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const railClasses = computed(() => [
|
|
104
|
+
props.options.railClass,
|
|
105
|
+
`osi-cl-rail--arrows-${arrowPosition.value.toLowerCase()}`,
|
|
106
|
+
`osi-cl-rail--dots-${dotsStyle.value.toLowerCase()}`,
|
|
107
|
+
{
|
|
108
|
+
"osi-cl-rail--has-dots": showDots.value,
|
|
109
|
+
"osi-cl-rail--arrows-hover": props.options.arrowsOnHover,
|
|
110
|
+
"osi-cl-rail--arrows-mobile": props.options.arrowsOnMobile,
|
|
111
|
+
},
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
return { perView, gap, loop, slides, onSwiper, onSlideChange, goPrev, goNext, isBeginning, isEnd, breakpoints, autoplay, overflows, arrowPosition, showArrows, showDots, dotsStyle, paginationKey, pagination, rail, arrowTop, observer, measureArrowTop, railStyle, railClasses, Autoplay, Pagination };
|
|
115
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
|
|
2
|
+
export default function useLogic({ props }) {
|
|
3
|
+
const root = ref(null);
|
|
4
|
+
const columns = ref(3);
|
|
5
|
+
|
|
6
|
+
function readColumns() {
|
|
7
|
+
const el = root.value;
|
|
8
|
+
if (!el) return;
|
|
9
|
+
const styles = getComputedStyle(el);
|
|
10
|
+
const phone = window.matchMedia("(max-width: 767px)").matches;
|
|
11
|
+
const n = Number(styles.getPropertyValue(phone ? "--osi-cl-ed-cols-mobile" : "--osi-cl-ed-cols"));
|
|
12
|
+
if (Number.isFinite(n) && n > 0) columns.value = Math.round(n);
|
|
13
|
+
}
|
|
14
|
+
onMounted(() => {
|
|
15
|
+
readColumns();
|
|
16
|
+
window.addEventListener("resize", readColumns, { passive: true });
|
|
17
|
+
});
|
|
18
|
+
onBeforeUnmount(() => window.removeEventListener("resize", readColumns));
|
|
19
|
+
// The column settings change in the builder without a resize.
|
|
20
|
+
onUpdated(readColumns);
|
|
21
|
+
|
|
22
|
+
const SHAPES = ["3 / 4", "1 / 1", "4 / 5"];
|
|
23
|
+
|
|
24
|
+
const stacks = computed(() => {
|
|
25
|
+
const out = Array.from({ length: columns.value }, () => []);
|
|
26
|
+
props.items.forEach((item, i) => {
|
|
27
|
+
// Shift the shape cycle one step per row: with as many columns as shapes,
|
|
28
|
+
// `i % shapes` alone would give every item in a column the same shape.
|
|
29
|
+
const row = Math.floor(i / columns.value);
|
|
30
|
+
const shape = SHAPES[(i + row) % SHAPES.length];
|
|
31
|
+
out[i % columns.value].push({
|
|
32
|
+
item,
|
|
33
|
+
// Overrides the card image's own shape (the template sets one for all).
|
|
34
|
+
style: props.vary ? { "--cb-ratio-override": shape, "--cb-h-override": "auto" } : null,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
return out;
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return { root, columns, readColumns, SHAPES, stacks };
|
|
41
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FEATURED — one category or collection, promoted.
|
|
3
|
+
*
|
|
4
|
+
* Covers both of the brief's designs. "Split" is media LEFT or RIGHT; "image
|
|
5
|
+
* overlay" is media BACKGROUND. They're one component because the only thing
|
|
6
|
+
* that changes is where the media sits relative to the text.
|
|
7
|
+
*
|
|
8
|
+
* Columns are `fr` units, not percentages: a percentage column plus a gap
|
|
9
|
+
* overflows its container, which is the same bug the footer had. `fr` resolves
|
|
10
|
+
* after the gap is subtracted.
|
|
11
|
+
*
|
|
12
|
+
* Link structure: the button is the real link. When the merchant turns the
|
|
13
|
+
* button off, the media becomes the link instead — so the section is never a
|
|
14
|
+
* dead end, and there's never more than one tab stop for the same destination.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
export default function useLogic({ props }) {
|
|
19
|
+
const HEIGHTS = {
|
|
20
|
+
AUTO: null,
|
|
21
|
+
SMALL: "320px",
|
|
22
|
+
MEDIUM: "460px",
|
|
23
|
+
LARGE: "620px",
|
|
24
|
+
FULL: "100svh",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const position = computed(() =>
|
|
28
|
+
String(props.options.mediaPosition ?? "RIGHT").toUpperCase()
|
|
29
|
+
);
|
|
30
|
+
const isBackground = computed(() => position.value === "BACKGROUND");
|
|
31
|
+
|
|
32
|
+
const columns = computed(() => {
|
|
33
|
+
const media = Math.min(75, Math.max(25, Number(props.options.mediaWidth) || 50));
|
|
34
|
+
return `${media}fr ${100 - media}fr`;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const style = computed(() => {
|
|
38
|
+
const out = {};
|
|
39
|
+
const height = HEIGHTS[String(props.options.height ?? "AUTO").toUpperCase()];
|
|
40
|
+
if (height) out["--osi-cf-height"] = height;
|
|
41
|
+
if (!isBackground.value) out.gridTemplateColumns = columns.value;
|
|
42
|
+
return out;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const classes = computed(() => [
|
|
46
|
+
`osi-cf--${position.value.toLowerCase()}`,
|
|
47
|
+
`osi-cf--v-${String(props.options.verticalAlignment ?? "CENTER").toLowerCase()}`,
|
|
48
|
+
{ "osi-cf--media-first": props.options.mobileMediaFirst !== false },
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const showDescription = computed(
|
|
52
|
+
() => props.config.showDescription && !!props.item.description
|
|
53
|
+
);
|
|
54
|
+
const showCount = computed(() => props.config.showCount && props.item.count != null);
|
|
55
|
+
|
|
56
|
+
return { HEIGHTS, position, isBackground, columns, style, classes, showDescription, showCount };
|
|
57
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The category / collection switcher on the listing page.
|
|
3
|
+
*
|
|
4
|
+
* One component, every look: plain text links, pills (theme buttons), image
|
|
5
|
+
* tiles (circle / square / rounded / portrait / landscape) with the name under,
|
|
6
|
+
* beside or on top of the picture, or cards — laid out as a scrolling row, a
|
|
7
|
+
* wrapping row, a grid, or a vertical list for the sidebar.
|
|
8
|
+
*
|
|
9
|
+
* Sizes and colours arrive as CSS variables set once by the section
|
|
10
|
+
* (--cnav-*), so dragging a slider in the editor doesn't re-render every tile.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
export default function useLogic({ props }) {
|
|
15
|
+
const lower = (v) => String(v || "").toLowerCase().replace(/_/g, "-");
|
|
16
|
+
|
|
17
|
+
const isList = computed(() => props.layout === "LIST");
|
|
18
|
+
const hasMedia = computed(() => props.variant === "IMAGE" || props.variant === "CARD");
|
|
19
|
+
const overlay = computed(
|
|
20
|
+
() => hasMedia.value && !isList.value && props.textPosition.startsWith("OVERLAY")
|
|
21
|
+
);
|
|
22
|
+
const captionOutside = computed(
|
|
23
|
+
() => !hasMedia.value || isList.value || ["BELOW", "BESIDE"].includes(props.textPosition)
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const rootClasses = computed(() => [
|
|
27
|
+
`osi-cnav--${lower(props.layout)}`,
|
|
28
|
+
`osi-cnav--${lower(props.variant)}`,
|
|
29
|
+
`osi-cnav--shape-${lower(props.shape)}`,
|
|
30
|
+
`osi-cnav--text-${lower(isList.value ? "BESIDE" : props.textPosition)}`,
|
|
31
|
+
`osi-cnav--active-${lower(props.activeStyle)}`,
|
|
32
|
+
`osi-cnav--hover-${lower(props.hover)}`,
|
|
33
|
+
`osi-cnav--fit-${lower(props.imageFit)}`,
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function pillClasses(item) {
|
|
37
|
+
if (props.variant !== "PILL" || isList.value) return [];
|
|
38
|
+
return ["osi-btn", "osi-btn--sm", item.active ? "" : "osi-btn--secondary"];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const initial = (title) => String(title || "?").trim().charAt(0).toUpperCase();
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A scrolling row starts at its first item, so a current item further along
|
|
45
|
+
* could be off screen on a phone. Bring it into view once, without moving the
|
|
46
|
+
* page itself.
|
|
47
|
+
*/
|
|
48
|
+
const list = ref(null);
|
|
49
|
+
function revealActive() {
|
|
50
|
+
const el = list.value;
|
|
51
|
+
if (!el || props.layout !== "SCROLL" || el.scrollWidth <= el.clientWidth) return;
|
|
52
|
+
const active = el.querySelector(".is-active");
|
|
53
|
+
if (!active) return;
|
|
54
|
+
const left = active.offsetLeft - (el.clientWidth - active.offsetWidth) / 2;
|
|
55
|
+
el.scrollTo({ left: Math.max(0, left), behavior: "auto" });
|
|
56
|
+
}
|
|
57
|
+
onMounted(() => nextTick(revealActive));
|
|
58
|
+
watch(() => props.items.map((i) => `${i.key}:${i.active}`).join(), () => nextTick(revealActive));
|
|
59
|
+
|
|
60
|
+
return { lower, isList, hasMedia, overlay, captionOutside, rootClasses, pillClasses, initial, list, revealActive };
|
|
61
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
|
|
2
|
+
export default function useLogic({ props }) {
|
|
3
|
+
const t = useT();
|
|
4
|
+
const plp = inject("osiPlp", null);
|
|
5
|
+
const { mediaUrl } = useMediaUrl();
|
|
6
|
+
const s = computed(() => props.node.s ?? {});
|
|
7
|
+
const type = computed(() => props.node.type);
|
|
8
|
+
|
|
9
|
+
const title = computed(() =>
|
|
10
|
+
plp?.kind.value === "ALL" ? t(s.value.ALL_TITLE) || "All products" : plp?.title.value || ""
|
|
11
|
+
);
|
|
12
|
+
const description = computed(() => plp?.description.value || "");
|
|
13
|
+
const lines = computed(() => {
|
|
14
|
+
const n = Number(s.value.LINES);
|
|
15
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
16
|
+
});
|
|
17
|
+
const crumbs = computed(() => {
|
|
18
|
+
const trail = (plp?.breadcrumbs.value ?? []).map((c) => ({ ...c }));
|
|
19
|
+
if (!trail.length) return [];
|
|
20
|
+
trail[0].title = t(s.value.HOME_LABEL) || trail[0].title || "Home";
|
|
21
|
+
trail[trail.length - 1].title = title.value;
|
|
22
|
+
return trail;
|
|
23
|
+
});
|
|
24
|
+
const imageSrc = computed(() => mediaUrl(s.value.MEDIA?.path ?? s.value.MEDIA ?? "") || plp?.image.value || "");
|
|
25
|
+
const px = (v, d) => `${Number.isFinite(Number(v)) && v !== "" && v != null ? Number(v) : d}px`;
|
|
26
|
+
const imageVars = computed(() => ({
|
|
27
|
+
"--cp-h": px(s.value.HEIGHT, 320),
|
|
28
|
+
"--cp-h-m": px(s.value.HEIGHT_MOBILE, 200),
|
|
29
|
+
"--cp-r": px(s.value.RADIUS, 0),
|
|
30
|
+
}));
|
|
31
|
+
const { isPreview } = useBuilderPreview();
|
|
32
|
+
|
|
33
|
+
/* ------------------------------------------------------------ search */
|
|
34
|
+
|
|
35
|
+
const router = useRouter();
|
|
36
|
+
const terms = computed(() => plp?.terms?.value ?? "");
|
|
37
|
+
const searchTitle = computed(() => {
|
|
38
|
+
const text = terms.value
|
|
39
|
+
? t(s.value.RESULTS_TITLE) || "Results for \u201c{terms}\u201d"
|
|
40
|
+
: t(s.value.TITLE) || "Search results";
|
|
41
|
+
return text.replaceAll("{terms}", terms.value);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const query = ref(terms.value);
|
|
45
|
+
watch(terms, (value) => (query.value = value));
|
|
46
|
+
|
|
47
|
+
const submit = computed(() => String(s.value.SUBMIT || "ICON").toUpperCase());
|
|
48
|
+
const SEARCH_JUSTIFY = { LEFT: "flex-start", CENTER: "center", RIGHT: "flex-end" };
|
|
49
|
+
const searchVars = computed(() => {
|
|
50
|
+
const n = (v) => (v === "" || v == null || !Number.isFinite(Number(v)) ? null : Number(v));
|
|
51
|
+
const out = { "--sb-max": `${n(s.value.MAX_WIDTH) ?? 560}px` };
|
|
52
|
+
const align = SEARCH_JUSTIFY[String(s.value.ALIGNMENT || "").toUpperCase()];
|
|
53
|
+
if (align) out["--sb-justify"] = align;
|
|
54
|
+
if (n(s.value.HEIGHT) != null) out["--sb-h"] = `${n(s.value.HEIGHT)}px`;
|
|
55
|
+
if (n(s.value.INPUT_RADIUS) != null) out["--in-radius"] = `${n(s.value.INPUT_RADIUS)}px`;
|
|
56
|
+
if (n(s.value.FONT_SIZE) != null) out["--in-fs"] = `${n(s.value.FONT_SIZE)}px`;
|
|
57
|
+
return out;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
function runSearch() {
|
|
61
|
+
const q = query.value.trim();
|
|
62
|
+
// The editor previews the page; it doesn't navigate.
|
|
63
|
+
if (isPreview.value || !q) return;
|
|
64
|
+
// A new search starts clean: no filters, sort or page from the last one.
|
|
65
|
+
router.push({ path: "/search", query: { q } });
|
|
66
|
+
}
|
|
67
|
+
function clearSearch() {
|
|
68
|
+
query.value = "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return { t, plp, mediaUrl, s, type, title, description, lines, crumbs, imageSrc, px, imageVars, isPreview, router, terms, searchTitle, query, submit, SEARCH_JUSTIFY, searchVars, runSearch, clearSearch };
|
|
72
|
+
}
|