@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.
Files changed (206) hide show
  1. package/README.md +80 -2
  2. package/THEME_DEV_API.md +80 -0
  3. package/app/app.vue +154 -0
  4. package/app/assets/css/osi-controls.css +824 -0
  5. package/app/components/SectionRenderer.vue +46 -0
  6. package/app/components/builder/BuilderOverlay.vue +582 -0
  7. package/app/components/checkout/ExtraFees.vue +329 -0
  8. package/app/components/checkout/PaymentHandoff.vue +388 -0
  9. package/app/components/common/Banner.vue +35 -0
  10. package/app/components/common/ConfirmDeleteModal.vue +117 -0
  11. package/app/components/common/DropdownSelect.vue +432 -0
  12. package/app/components/common/Loader.vue +32 -0
  13. package/app/components/common/OsiField.vue +218 -0
  14. package/app/components/common/PhoneNumInput.vue +434 -0
  15. package/app/composables/UseBodyScrollLock.js +122 -0
  16. package/app/composables/UseThemeGroup.js +575 -0
  17. package/app/composables/useAddToCart.js +87 -0
  18. package/app/composables/useAnalytics.js +15 -0
  19. package/app/composables/useAnnouncements.js +29 -0
  20. package/app/composables/useApiErrorToast.js +104 -0
  21. package/app/composables/useAuth.js +99 -0
  22. package/app/composables/useBanners.js +25 -0
  23. package/app/composables/useBuilderPreview.js +320 -0
  24. package/app/composables/useButtonStyle.js +107 -0
  25. package/app/composables/useCarousel.js +160 -0
  26. package/app/composables/useCart.js +109 -0
  27. package/app/composables/useCheckout.js +76 -0
  28. package/app/composables/useClassificationItems.js +269 -0
  29. package/app/composables/useClassificationPage.js +545 -0
  30. package/app/composables/useClassificationUrl.js +63 -0
  31. package/app/composables/useColorScheme.js +44 -0
  32. package/app/composables/useContactForm.js +53 -0
  33. package/app/composables/useContentTree.js +770 -0
  34. package/app/composables/useDragScroll.js +73 -0
  35. package/app/composables/useFooterSocials.js +107 -0
  36. package/app/composables/useInputStyle.js +45 -0
  37. package/app/composables/useLayoutShell.js +114 -0
  38. package/app/composables/useLazyVideo.js +36 -0
  39. package/app/composables/useMediaTypes.js +66 -0
  40. package/app/composables/useMediaUrl.js +71 -0
  41. package/app/composables/useMenu.js +100 -0
  42. package/app/composables/useNavStyle.js +219 -0
  43. package/app/composables/usePlaceholders.js +294 -0
  44. package/app/composables/usePolicies.js +26 -0
  45. package/app/composables/useProductCardMedia.js +135 -0
  46. package/app/composables/useProductCardStyle.js +132 -0
  47. package/app/composables/useProductDetail.js +982 -0
  48. package/app/composables/useProductGallery.js +166 -0
  49. package/app/composables/useProductItems.js +270 -0
  50. package/app/composables/useProductUrl.js +60 -0
  51. package/app/composables/useProductVariants.js +210 -0
  52. package/app/composables/useQuickShop.js +115 -0
  53. package/app/composables/useRelatedProducts.js +186 -0
  54. package/app/composables/useRenderContext.js +45 -0
  55. package/app/composables/useScopedCss.js +106 -0
  56. package/app/composables/useSearch.js +35 -0
  57. package/app/composables/useSectionData.js +462 -0
  58. package/app/composables/useSectionShell.js +113 -0
  59. package/app/composables/useSidebar.js +69 -0
  60. package/app/composables/useStoreData.js +57 -0
  61. package/app/composables/useStoreInit.js +52 -0
  62. package/app/composables/useStorefront.js +239 -0
  63. package/app/composables/useT.js +21 -0
  64. package/app/composables/useTextStyles.js +84 -0
  65. package/app/composables/useThemeControls.js +34 -0
  66. package/app/composables/useThemeSeo.js +91 -0
  67. package/app/composables/useThemeTokens.js +171 -0
  68. package/app/composables/useWishlist.js +66 -0
  69. package/app/logic/components/AnnouncementBar.js +118 -0
  70. package/app/logic/components/CardGrid.js +125 -0
  71. package/app/logic/components/CardList.js +98 -0
  72. package/app/logic/components/CartDrawer.js +273 -0
  73. package/app/logic/components/CartItem.js +31 -0
  74. package/app/logic/components/ClassificationBento.js +6 -0
  75. package/app/logic/components/ClassificationCard.js +6 -0
  76. package/app/logic/components/ClassificationCarousel.js +115 -0
  77. package/app/logic/components/ClassificationEditorial.js +41 -0
  78. package/app/logic/components/ClassificationFeatured.js +57 -0
  79. package/app/logic/components/ClassificationGrid.js +6 -0
  80. package/app/logic/components/ClassificationNav.js +61 -0
  81. package/app/logic/components/ClassificationPart.js +72 -0
  82. package/app/logic/components/FooterBlock.js +91 -0
  83. package/app/logic/components/FooterSocials.js +60 -0
  84. package/app/logic/components/HeaderDropdownList.js +29 -0
  85. package/app/logic/components/HeaderIcon.js +68 -0
  86. package/app/logic/components/HeaderLogo.js +39 -0
  87. package/app/logic/components/HeaderNav.js +268 -0
  88. package/app/logic/components/MobileMenu.js +280 -0
  89. package/app/logic/components/ProductBuyButtons.js +66 -0
  90. package/app/logic/components/ProductCard.js +160 -0
  91. package/app/logic/components/ProductFilters.js +37 -0
  92. package/app/logic/components/ProductGallery.js +19 -0
  93. package/app/logic/components/ProductInfoBlock.js +411 -0
  94. package/app/logic/components/ProductLightbox.js +12 -0
  95. package/app/logic/components/ProductQuantity.js +31 -0
  96. package/app/logic/components/ProductVariantPicker.js +125 -0
  97. package/app/logic/components/QuickBuy.js +194 -0
  98. package/app/logic/components/SearchOverlay.js +41 -0
  99. package/app/logic/components/SidebarMenuItem.js +39 -0
  100. package/app/logic/components/SiteFooter.js +476 -0
  101. package/app/logic/components/SiteHeader.js +266 -0
  102. package/app/logic/components/WhatsAppButton.js +75 -0
  103. package/app/logic/layouts/default.js +66 -0
  104. package/app/logic/sections/AnnouncementBarSection.js +6 -0
  105. package/app/logic/sections/BlockCarousel.js +103 -0
  106. package/app/logic/sections/ClassificationListSection.js +40 -0
  107. package/app/logic/sections/ContactFormSection.js +294 -0
  108. package/app/logic/sections/ContactInfoSection.js +419 -0
  109. package/app/logic/sections/ContentNode.js +189 -0
  110. package/app/logic/sections/CountdownSection.js +254 -0
  111. package/app/logic/sections/CustomHtmlSection.js +51 -0
  112. package/app/logic/sections/DividerSection.js +31 -0
  113. package/app/logic/sections/FaqSection.js +53 -0
  114. package/app/logic/sections/FeatureListSection.js +6 -0
  115. package/app/logic/sections/FeaturedProductSection.js +40 -0
  116. package/app/logic/sections/FooterSection.js +16 -0
  117. package/app/logic/sections/FooterUtilitiesSection.js +24 -0
  118. package/app/logic/sections/GallerySection.js +232 -0
  119. package/app/logic/sections/HeaderSection.js +24 -0
  120. package/app/logic/sections/HeroSection.js +24 -0
  121. package/app/logic/sections/LogoListSection.js +6 -0
  122. package/app/logic/sections/MainClassificationSection.js +473 -0
  123. package/app/logic/sections/MainProductSection.js +211 -0
  124. package/app/logic/sections/MainSearchSection.js +6 -0
  125. package/app/logic/sections/MapSection.js +92 -0
  126. package/app/logic/sections/MediaContentSection.js +38 -0
  127. package/app/logic/sections/MediaWithTextSection.js +238 -0
  128. package/app/logic/sections/NewsletterSection.js +126 -0
  129. package/app/logic/sections/ProductHighlightSection.js +201 -0
  130. package/app/logic/sections/ProductListSection.js +45 -0
  131. package/app/logic/sections/ProductRecommendationsSection.js +113 -0
  132. package/app/logic/sections/RichTextSection.js +6 -0
  133. package/app/logic/sections/ScrollingTextSection.js +69 -0
  134. package/app/logic/sections/SearchHeaderSection.js +22 -0
  135. package/app/logic/sections/SlideshowSection.js +118 -0
  136. package/app/logic/sections/SpacerSection.js +37 -0
  137. package/app/logic/sections/SplitShowcaseSection.js +6 -0
  138. package/app/logic/sections/TestimonialListSection.js +6 -0
  139. package/app/logic/sections/VideoSection.js +235 -0
  140. package/app/pages/[category]/[product].vue +196 -0
  141. package/app/pages/__builder-preview.vue +38 -0
  142. package/app/pages/categories/[slug].vue +208 -0
  143. package/app/pages/checkout.vue +1972 -0
  144. package/app/pages/forgot-password.vue +115 -0
  145. package/app/pages/index.vue +64 -0
  146. package/app/pages/login.vue +585 -0
  147. package/app/pages/order-detail.vue +442 -0
  148. package/app/pages/pages/[handle].vue +58 -0
  149. package/app/pages/privacy-policy.vue +46 -0
  150. package/app/pages/resetpassword.vue +138 -0
  151. package/app/pages/resetverificationcode.vue +163 -0
  152. package/app/pages/result.vue +203 -0
  153. package/app/pages/return-policy.vue +50 -0
  154. package/app/pages/search.vue +100 -0
  155. package/app/pages/shipping-policy.vue +46 -0
  156. package/app/pages/user-profile/components/AddAddressModal.vue +452 -0
  157. package/app/pages/user-profile/components/Whishlist.vue +137 -0
  158. package/app/pages/user-profile/components/left-bar.vue +233 -0
  159. package/app/pages/user-profile/components/orders.vue +162 -0
  160. package/app/pages/user-profile/components/profile-details.vue +566 -0
  161. package/app/pages/user-profile/components/settings.vue +194 -0
  162. package/app/pages/user-profile/index.vue +252 -0
  163. package/app/pages/verificationcode.vue +188 -0
  164. package/app/plugins/analytics.client.js +99 -0
  165. package/app/plugins/geo.ts +24 -0
  166. package/app/plugins/reveal.client.js +53 -0
  167. package/app/plugins/toast.js +10 -0
  168. package/app/plugins/vue-query.ts +5 -0
  169. package/app/services/ApiServices.js +74 -0
  170. package/app/services/AuthBus.js +162 -0
  171. package/app/services/Authentication/AuthenticationService.js +133 -0
  172. package/app/services/Authentication/TokenService.js +62 -0
  173. package/app/services/CartServices.js +91 -0
  174. package/app/services/CartToken.js +26 -0
  175. package/app/services/CategoryServices.js +54 -0
  176. package/app/services/CheckoutServices.js +113 -0
  177. package/app/services/HomeServices.js +133 -0
  178. package/app/services/NewsletterServices.js +29 -0
  179. package/app/services/PaymentServices.js +71 -0
  180. package/app/services/PoliciesServices.js +16 -0
  181. package/app/services/ProductServices.js +79 -0
  182. package/app/services/SearchServices.js +18 -0
  183. package/app/services/UserServices.js +105 -0
  184. package/app/services/UtilsServices.js +46 -0
  185. package/app/services/WishlistServices.js +51 -0
  186. package/app/services/storefront.service.js +57 -0
  187. package/app/stores/userStore.js +13 -0
  188. package/app/theme/categoryTree.js +68 -0
  189. package/app/theme/controls.js +88 -0
  190. package/app/theme/registry.js +56 -0
  191. package/app/utils/analytics.js +864 -0
  192. package/app/utils/device.js +50 -0
  193. package/app/utils/eventBus.js +24 -0
  194. package/app/utils/icon-presets.js +24 -0
  195. package/app/utils/productOffers.js +118 -0
  196. package/app/utils/requestCache.js +36 -0
  197. package/app/utils/scrollToError.js +59 -0
  198. package/app/utils/toPlainError.js +30 -0
  199. package/bin/osimart-theme.mjs +259 -0
  200. package/nuxt/osi-sections.ts +56 -0
  201. package/nuxt.config.ts +60 -0
  202. package/package.json +43 -3
  203. package/public/css/input.css +29 -0
  204. package/public/css/output.css +2716 -0
  205. package/public/favicon-32x32.png +0 -0
  206. package/public/robots.txt +2 -0
@@ -0,0 +1,171 @@
1
+ import {
2
+ FONT_FAMILIES,
3
+ TEXT_TRANSFORMS,
4
+ color,
5
+ compact,
6
+ letterSpacing,
7
+ px,
8
+ shadow,
9
+ weight,
10
+ } from "#osi/theme/controls";
11
+
12
+ /**
13
+ * Theme settings -> CSS custom properties.
14
+ *
15
+ * Every component styles itself from these variables. Nothing reads
16
+ * theme.HEADING_FONT directly — that's what lets a merchant change a font and
17
+ * have the whole storefront follow without a single component knowing.
18
+ *
19
+ * Spread onto the root element in app.vue, together with `rootClasses`.
20
+ *
21
+ * FIXED FONTS
22
+ *
23
+ * A hand-built store (stores/<name>) can pin its fonts in app.config.ts:
24
+ * fonts: { heading: 'Avenir', body: 'Avenir' }
25
+ * A pinned font wins over the merchant's HEADING_FONT / BODY_FONT, and isn't
26
+ * loaded from Google — the store ships it itself (@font-face in its CSS).
27
+ *
28
+ * BUTTONS / INPUTS
29
+ *
30
+ * The --osi-button-* and --osi-input-* sets are what every cart, login,
31
+ * checkout, account and section control reads (assets/css/osi-controls.css).
32
+ * Colours are only emitted when the merchant set one: an unset button colour
33
+ * has to keep following the section's colour scheme (--osi-btn-bg), and it
34
+ * can't if the theme pins it here.
35
+ *
36
+ * The three enum settings (button style, hover effect, field style, label
37
+ * position) can't be variables — CSS can't branch on one — so they become
38
+ * classes on the root instead.
39
+ */
40
+ export function useThemeTokens() {
41
+ const { theme, store } = useStorefront();
42
+ const fixedFonts = useAppConfig().fonts ?? {};
43
+
44
+ // Pages outside the section renderer (login, checkout, account, the cart
45
+ // drawer) never get a section's scheme variables, so the default scheme is
46
+ // published on the root too. Sections still override it on their own root.
47
+ const defaultScheme = useColorScheme(computed(() => null));
48
+
49
+ const tokens = computed(() => {
50
+ const t = theme.value ?? {};
51
+ const s = store.value ?? {};
52
+
53
+ const headingFont = fixedFonts.heading ?? t.HEADING_FONT ?? "Poppins";
54
+ const bodyFont = fixedFonts.body ?? t.BODY_FONT ?? "DM Sans";
55
+
56
+ return compact({
57
+ ...defaultScheme.value.style,
58
+
59
+ "--primary-color": s.primary_color ?? "#000000",
60
+ "--secondary-color": s.secondary_color ?? "#f3f4f6",
61
+ "--osi-primary-color": s.primary_color ?? "#000000",
62
+ "--osi-secondary-color": s.secondary_color ?? "#f3f4f6",
63
+
64
+ "--osi-font-heading": `"${headingFont}", system-ui, sans-serif`,
65
+ "--osi-font-body": `"${bodyFont}", system-ui, sans-serif`,
66
+ "--osi-heading-scale": (t.HEADING_SCALE ?? 100) / 100,
67
+ "--osi-body-size": `${((t.BODY_SCALE ?? 100) / 100) * 16}px`,
68
+
69
+ "--osi-page-width": `${t.PAGE_WIDTH ?? 1400}px`,
70
+ "--osi-section-gap": `${t.SECTION_SPACING ?? 48}px`,
71
+ "--osi-grid-gap": `${t.GRID_GAP ?? 24}px`,
72
+
73
+ "--osi-logo-width": `${t.LOGO_WIDTH ?? 120}px`,
74
+
75
+ // ------------------------------------------------------------ buttons
76
+ // Legacy names, still read by a few component stylesheets.
77
+ "--osi-btn-radius": px(t.BUTTON_RADIUS) ?? "4px",
78
+ "--osi-btn-border": px(t.BUTTON_BORDER_WIDTH) ?? "1px",
79
+ "--osi-btn-shadow": shadow(t.BUTTON_SHADOW, t.BUTTON_SHADOW_COLOR) ?? "none",
80
+
81
+ "--osi-button-radius": px(t.BUTTON_RADIUS) ?? "4px",
82
+ "--osi-button-border-w": px(t.BUTTON_BORDER_WIDTH) ?? "1px",
83
+ "--osi-button-shadow": shadow(t.BUTTON_SHADOW, t.BUTTON_SHADOW_COLOR) ?? "none",
84
+ "--osi-button-hover-shadow": shadow(t.BUTTON_HOVER_SHADOW, t.BUTTON_SHADOW_COLOR),
85
+ "--osi-button-pad-y": px(t.BUTTON_PADDING_Y) ?? "12px",
86
+ "--osi-button-pad-x": px(t.BUTTON_PADDING_X) ?? "28px",
87
+ "--osi-button-pad-y-mobile": px(t.BUTTON_PADDING_Y_MOBILE),
88
+ "--osi-button-pad-x-mobile": px(t.BUTTON_PADDING_X_MOBILE),
89
+ "--osi-button-fs": px(t.BUTTON_FONT_SIZE) ?? "14px",
90
+ "--osi-button-fw": weight(t.BUTTON_FONT_WEIGHT) ?? 500,
91
+ "--osi-button-ff": FONT_FAMILIES[t.BUTTON_FONT] ?? "inherit",
92
+ "--osi-button-tt": TEXT_TRANSFORMS[t.BUTTON_TRANSFORM] ?? "none",
93
+ "--osi-button-ls": letterSpacing(t.BUTTON_LETTER_SPACING) ?? "normal",
94
+ "--osi-button-bg": color(t.BUTTON_BG_COLOR),
95
+ "--osi-button-fg": color(t.BUTTON_TEXT_COLOR),
96
+ "--osi-button-border-color": color(t.BUTTON_BORDER_COLOR),
97
+ "--osi-button-hover-bg": color(t.BUTTON_HOVER_BG_COLOR),
98
+ "--osi-button-hover-fg": color(t.BUTTON_HOVER_TEXT_COLOR),
99
+ "--osi-button-hover-border-color": color(t.BUTTON_HOVER_BORDER_COLOR),
100
+
101
+ "--osi-button2-bg": color(t.SECONDARY_BUTTON_BG_COLOR),
102
+ "--osi-button2-fg": color(t.SECONDARY_BUTTON_TEXT_COLOR),
103
+ "--osi-button2-border-color": color(t.SECONDARY_BUTTON_BORDER_COLOR),
104
+
105
+ // ------------------------------------------------------------- inputs
106
+ "--osi-input-radius": px(t.INPUT_RADIUS) ?? "4px",
107
+ "--osi-input-border-w": px(t.INPUT_BORDER_WIDTH) ?? "1px",
108
+ "--osi-input-shadow": shadow(t.INPUT_SHADOW, t.INPUT_SHADOW_COLOR) ?? "none",
109
+ "--osi-input-pad-y": px(t.INPUT_PADDING_Y) ?? "10px",
110
+ "--osi-input-pad-x": px(t.INPUT_PADDING_X) ?? "12px",
111
+ "--osi-input-pad-y-mobile": px(t.INPUT_PADDING_Y_MOBILE),
112
+ "--osi-input-pad-x-mobile": px(t.INPUT_PADDING_X_MOBILE),
113
+ "--osi-input-fs": px(t.INPUT_FONT_SIZE) ?? "15px",
114
+ "--osi-input-fs-mobile": px(t.INPUT_FONT_SIZE_MOBILE),
115
+ "--osi-input-bg": color(t.INPUT_BG_COLOR),
116
+ "--osi-input-fg": color(t.INPUT_TEXT_COLOR),
117
+ "--osi-input-border-color": color(t.INPUT_BORDER_COLOR),
118
+ "--osi-input-focus": color(t.INPUT_FOCUS_COLOR),
119
+ "--osi-input-placeholder": color(t.INPUT_PLACEHOLDER_COLOR),
120
+ "--osi-input-error": color(t.INPUT_ERROR_COLOR),
121
+ "--osi-input-gap": px(t.INPUT_GAP),
122
+
123
+ "--osi-label-fs": px(t.INPUT_LABEL_SIZE),
124
+ "--osi-label-fw": weight(t.INPUT_LABEL_WEIGHT),
125
+ "--osi-label-tt": TEXT_TRANSFORMS[t.INPUT_LABEL_TRANSFORM],
126
+ "--osi-label-ls": letterSpacing(t.INPUT_LABEL_LETTER_SPACING),
127
+ "--osi-label-color": color(t.INPUT_LABEL_COLOR),
128
+ });
129
+ });
130
+
131
+ const { controls, rootClasses } = useThemeControls();
132
+
133
+ /**
134
+ * Fonts are arbitrary merchant-chosen strings, so they're loaded at runtime
135
+ * rather than bundled. If the store has an Arabic locale, load an Arabic
136
+ * subset too — Poppins has no Arabic glyphs and the fallback looks broken.
137
+ * Pinned fonts (see FIXED FONTS) are skipped: the store ships them itself.
138
+ */
139
+ const fontLinks = computed(() => {
140
+ const t = theme.value ?? {};
141
+ const families = [
142
+ ...new Set(
143
+ [
144
+ fixedFonts.heading ? null : t.HEADING_FONT,
145
+ fixedFonts.body ? null : t.BODY_FONT,
146
+ ].filter(Boolean),
147
+ ),
148
+ ];
149
+ if (!families.length) return [];
150
+ const css2 = (spec) => `https://fonts.googleapis.com/css2?family=${spec}&display=swap`;
151
+ // Two stylesheets per family, never one shared request: Google answers
152
+ // 400 for the WHOLE request when a family lacks any listed weight (Bebas
153
+ // Neue has 400 only), which silently dropped every font. The plain one
154
+ // always loads (regular); the weighted one adds the rest where they
155
+ // exist and fails alone where they don't.
156
+ const sheets = families.flatMap((family) => {
157
+ const name = encodeURIComponent(family).replace(/%20/g, "+");
158
+ return [
159
+ { rel: "stylesheet", href: css2(name), key: `osi-font-${family}` },
160
+ { rel: "stylesheet", href: css2(`${name}:wght@300;400;500;600;700`), key: `osi-font-${family}-w` },
161
+ ];
162
+ });
163
+ return [
164
+ { rel: "preconnect", href: "https://fonts.googleapis.com", key: "osi-font-pc1" },
165
+ { rel: "preconnect", href: "https://fonts.gstatic.com", crossorigin: "", key: "osi-font-pc2" },
166
+ ...sheets,
167
+ ];
168
+ });
169
+
170
+ return { tokens, rootClasses, controls, fontLinks };
171
+ }
@@ -0,0 +1,66 @@
1
+ import WishlistServices from "#osi/services/WishlistServices";
2
+ import { authBus } from "#osi/services/AuthBus";
3
+ import { analytics } from "#osi/utils/analytics";
4
+ import { useToast } from "vue-toastification";
5
+
6
+ export function useWishlist() {
7
+ const wishlistStatus = useState("wishlist-status", () => ({}));
8
+ const wishlistedItems = useState("wishlist-items", () => []);
9
+
10
+ const isLoggedIn = computed(() => authBus.isLoggedIn);
11
+
12
+ async function fetchWishlist() {
13
+ if (!isLoggedIn.value) return;
14
+ try {
15
+ const response = await WishlistServices.getWishlists();
16
+ wishlistedItems.value = response?.results || response || [];
17
+ } catch (error) {
18
+ console.error("[useWishlist] Error fetching wishlist:", error);
19
+ }
20
+ }
21
+
22
+ async function addToWishlist(product) {
23
+ const toast = useToast();
24
+ const productId = product.id;
25
+
26
+ if (!isLoggedIn.value) {
27
+ toast.error("Please log in to add items to your wishlist.");
28
+ return;
29
+ }
30
+
31
+ // Optimistic UI Update
32
+ const previousStatus = wishlistStatus.value[productId] ?? product.wishlisted;
33
+ wishlistStatus.value[productId] = !previousStatus;
34
+ product.wishlisted = !previousStatus;
35
+
36
+ try {
37
+ let response;
38
+ if (previousStatus) {
39
+ response = await WishlistServices.removeFromWishlist(productId);
40
+ toast.success("Product removed from wishlist");
41
+ } else {
42
+ response = await WishlistServices.addToWishlist(productId);
43
+ toast.success("Product added to wishlist");
44
+ analytics.addToWishlist(product);
45
+ }
46
+
47
+ if (response && response.wishlisted !== undefined) {
48
+ wishlistStatus.value[productId] = response.wishlisted;
49
+ product.wishlisted = response.wishlisted;
50
+ }
51
+ } catch (error) {
52
+ console.error("[useWishlist] Error updating wishlist:", error);
53
+ toast.error("An error occurred while updating your wishlist.");
54
+ wishlistStatus.value[productId] = previousStatus;
55
+ product.wishlisted = previousStatus;
56
+ }
57
+ }
58
+
59
+ return {
60
+ wishlistStatus,
61
+ wishlistedItems,
62
+ isLoggedIn,
63
+ fetchWishlist,
64
+ addToWishlist,
65
+ };
66
+ }
@@ -0,0 +1,118 @@
1
+ import { withAsyncContext } from "vue";
2
+ import { Autoplay, Navigation, EffectFade } from "swiper/modules";
3
+
4
+ export default async function useLogic({ props }) {
5
+ let __temp, __restore;
6
+
7
+ const settings = useThemeGroup("ANNOUNCEMENT");
8
+ const scheme = useColorScheme(computed(() => settings.value.ANNOUNCEMENT_COLOR_SCHEME));
9
+ const { isMobile } = useLayoutShell();
10
+ const t = useT();
11
+
12
+ const px = (v) => (v === "" || v == null || !Number.isFinite(Number(v)) ? undefined : `${Number(v)}px`);
13
+ const WEIGHTS = { LIGHT: 300, REGULAR: 400, MEDIUM: 500, SEMIBOLD: 600, BOLD: 700 };
14
+ const weightOf = (v) => {
15
+ const n = Number(v);
16
+ return Number.isFinite(n) && n > 0 ? n : WEIGHTS[String(v || "").toUpperCase()];
17
+ };
18
+ const CASES = { NONE: "none", UPPERCASE: "uppercase", LOWERCASE: "lowercase", CAPITALIZE: "capitalize" };
19
+
20
+ const blockMessages = computed(() =>
21
+ (props.blocks ?? [])
22
+ .filter((b) => b.type === "ANNOUNCEMENT" && !b.disabled)
23
+ .map((b) => {
24
+ const v = b.settings ?? {};
25
+ const style = Object.fromEntries(Object.entries({
26
+ "--ann-fs": px(v.SIZE),
27
+ "--ann-fs-m": px(v.SIZE_MOBILE) ?? px(v.SIZE),
28
+ fontFamily: String(v.FONT || "").toUpperCase() === "HEADING" ? "var(--osi-font-heading)" : undefined,
29
+ fontWeight: weightOf(v.WEIGHT),
30
+ letterSpacing: v.LETTER_SPACING ? `${Number(v.LETTER_SPACING) / 100}em` : undefined,
31
+ textTransform: CASES[String(v.CASE || "").toUpperCase()],
32
+ color: v.COLOR || undefined,
33
+ textDecoration: v.URL && v.UNDERLINE ? "underline" : undefined,
34
+ }).filter(([, x]) => x !== undefined));
35
+ return {
36
+ id: b.key,
37
+ key: b.key,
38
+ text: t(v.TEXT),
39
+ url: v.URL || null,
40
+ newTab: !!v.OPEN_IN_NEW_TAB,
41
+ background: v.BACKGROUND || null,
42
+ style,
43
+ };
44
+ })
45
+ .filter((m) => m.text)
46
+ );
47
+
48
+ // Only without blocks: the store's announcements.
49
+ const useBlocks = computed(() => blockMessages.value.length > 0);
50
+ const { announcements, loadAnnouncements } = useAnnouncements();
51
+ if (!useBlocks.value) (([__temp, __restore] = withAsyncContext(() => loadAnnouncements())), (__temp = await __temp), __restore(), __temp);
52
+
53
+ const apiMessages = computed(() => {
54
+ const raw = announcements.value;
55
+ const list = Array.isArray(raw) ? raw : raw?.results ?? [];
56
+ return list
57
+ .map((entry) => ({
58
+ id: entry.id,
59
+ key: null,
60
+ text: t(entry.text ?? entry.message ?? entry.title),
61
+ url: entry.url || entry.link || null,
62
+ newTab: false,
63
+ background: null,
64
+ style: {},
65
+ }))
66
+ .filter((entry) => entry.text);
67
+ });
68
+
69
+ const messages = computed(() => (useBlocks.value ? blockMessages.value : apiMessages.value));
70
+
71
+ const visible = computed(
72
+ () => settings.value.ANNOUNCEMENT_ENABLED !== false && messages.value.length > 0
73
+ );
74
+
75
+ const { slides, onSwiper, onSlideChange, activeIndex, setNavigationEnabled, goPrev, goNext } = useCarousel({
76
+ items: () => messages.value,
77
+ loop: () => messages.value.length > 1,
78
+ minLoopSlides: 3,
79
+ });
80
+
81
+ const fade = computed(() => String(settings.value.ANNOUNCEMENT_TRANSITION || "SLIDE").toUpperCase() === "FADE");
82
+ const modules = [Autoplay, Navigation, EffectFade];
83
+
84
+ const autoplay = computed(() =>
85
+ settings.value.ANNOUNCEMENT_AUTOPLAY !== false && messages.value.length > 1
86
+ ? { delay: (settings.value.ANNOUNCEMENT_SPEED || 5) * 1000, disableOnInteraction: false }
87
+ : false
88
+ );
89
+
90
+ // Arrows on phones (ANNOUNCEMENT_SHOW_ARROWS_MOBILE) and desktop
91
+ // (ANNOUNCEMENT_SHOW_ARROWS), each switchable. Swiper needs
92
+ // navigation.init()/destroy(), not just the param flipped, which is what
93
+ // setNavigationEnabled does.
94
+ const showArrows = computed(
95
+ () =>
96
+ messages.value.length > 1 &&
97
+ (isMobile.value
98
+ ? settings.value.ANNOUNCEMENT_SHOW_ARROWS_MOBILE !== false
99
+ : settings.value.ANNOUNCEMENT_SHOW_ARROWS !== false)
100
+ );
101
+ watch(showArrows, (enabled) => setNavigationEnabled(enabled), { immediate: true });
102
+
103
+ /** The bar: its scheme, the showing message's own background, padding, width, line. */
104
+ const barStyle = computed(() => {
105
+ const current = slides.value[activeIndex.value] ?? messages.value[0];
106
+ const s = settings.value;
107
+ return {
108
+ ...scheme.value.style,
109
+ background: current?.background || "var(--osi-bg)",
110
+ color: "var(--osi-text)",
111
+ "--ann-py": px(s.ANNOUNCEMENT_PADDING_Y) ?? "15px",
112
+ "--ann-max": String(s.ANNOUNCEMENT_WIDTH || "PAGE").toUpperCase() === "FULL" ? "none" : "var(--osi-page-width)",
113
+ borderBottom: s.ANNOUNCEMENT_DIVIDER ? "1px solid var(--osi-border)" : undefined,
114
+ };
115
+ });
116
+
117
+ return { settings, scheme, isMobile, t, px, WEIGHTS, weightOf, CASES, blockMessages, useBlocks, announcements, loadAnnouncements, apiMessages, messages, visible, slides, onSwiper, onSlideChange, activeIndex, setNavigationEnabled, goPrev, goNext, fade, modules, autoplay, showArrows, barStyle };
118
+ }
@@ -0,0 +1,125 @@
1
+ import { NuxtLink } from "#components";
2
+ import { buttonClasses as toButtonClasses, buttonVars } from "#osi/composables/useButtonStyle";
3
+
4
+ export default function useLogic({ props }) {
5
+ const { s, t, shellStyle, innerStyle, align, customClass, heading } = useSectionShell(() => props.section);
6
+ const { mediaUrl } = useMediaUrl();
7
+ const { isMobile } = useLayoutShell();
8
+ const placeholders = usePlaceholders();
9
+ const NuxtLinkComponent = NuxtLink;
10
+
11
+ const isPromo = computed(() => props.variant === "promo");
12
+ const has = (v) => v !== undefined && v !== null && v !== "";
13
+ const num = (v, d) => (has(v) && Number.isFinite(Number(v)) ? Number(v) : d);
14
+ const px = (v) => (has(v) && Number.isFinite(Number(v)) ? `${Number(v)}px` : undefined);
15
+
16
+ const VIDEO_EXT = /\.(mp4|webm|mov|m4v)(\?.*)?$/i;
17
+ const isVideoUrl = (url) => !!url && VIDEO_EXT.test(url);
18
+
19
+ const RATIOS = { SQUARE: "1 / 1", PORTRAIT: "3 / 4", PORTRAIT_4_5: "4 / 5", LANDSCAPE: "4 / 3", WIDE: "16 / 9", NATURAL: "auto" };
20
+ const SPOTS = {
21
+ TOP_LEFT: ["flex-start", "flex-start"], TOP_CENTER: ["flex-start", "center"], TOP_RIGHT: ["flex-start", "flex-end"],
22
+ CENTER_LEFT: ["center", "flex-start"], CENTER: ["center", "center"], CENTER_RIGHT: ["center", "flex-end"],
23
+ BOTTOM_LEFT: ["flex-end", "flex-start"], BOTTOM_CENTER: ["flex-end", "center"], BOTTOM_RIGHT: ["flex-end", "flex-end"],
24
+ };
25
+ const TEXT_ALIGN = { LEFT: "start", CENTER: "center", RIGHT: "end" };
26
+ const ITEMS = { LEFT: "flex-start", CENTER: "center", RIGHT: "flex-end" };
27
+
28
+ const defaultPosition = computed(() => (isPromo.value ? "OVERLAY" : "BELOW"));
29
+
30
+ const cards = computed(() => {
31
+ const rows = (props.section.blocks ?? [])
32
+ .filter((b) => !b.disabled)
33
+ .map((b) => {
34
+ const bs = b.settings ?? {};
35
+ const media = isMobile.value ? bs.MOBILE_MEDIA || bs.MEDIA : bs.MEDIA;
36
+ const src = mediaUrl(media ?? b.media ?? null);
37
+ const position = String(bs.CARD_TEXT_POSITION || s.value.TEXT_POSITION || defaultPosition.value).toUpperCase();
38
+ const buttonLabel = t(bs.BUTTON_LABEL);
39
+ return {
40
+ key: b.id ?? b.key,
41
+ src,
42
+ isVideo: isVideoUrl(src),
43
+ poster: mediaUrl(bs.POSTER ?? null) || null,
44
+ alt: t(bs.ALT) || t(bs.HEADING) || "",
45
+ eyebrow: t(bs.EYEBROW),
46
+ heading: t(bs.HEADING),
47
+ text: t(bs.TEXT),
48
+ url: bs.URL || null,
49
+ button: buttonLabel && bs.URL
50
+ ? { label: buttonLabel, style: buttonVars(bs, "BUTTON"), classes: toButtonClasses(bs, "BUTTON", ["osi-ig-btn"]) }
51
+ : null,
52
+ overlay: position === "OVERLAY",
53
+ style: {
54
+ "--ig-span-c": num(bs.SPAN_COLUMNS, 1),
55
+ "--ig-span-r": num(bs.SPAN_ROWS, 1),
56
+ ...(has(bs.OVERLAY_OPACITY) ? { "--ig-ov-opacity": String(Number(bs.OVERLAY_OPACITY) / 100) } : {}),
57
+ },
58
+ };
59
+ });
60
+
61
+ if (!rows.length && placeholders.allowed?.value) {
62
+ return Array.from({ length: isPromo.value ? 2 : 3 }, (_, i) => ({
63
+ key: `ph-${i}`,
64
+ src: null,
65
+ isVideo: false,
66
+ poster: null,
67
+ alt: "",
68
+ eyebrow: "",
69
+ heading: isPromo.value ? "Promotion title" : "Card title",
70
+ text: "",
71
+ url: null,
72
+ button: null,
73
+ overlay: defaultPosition.value === "OVERLAY",
74
+ style: {},
75
+ placeholder: true,
76
+ }));
77
+ }
78
+ return rows;
79
+ });
80
+
81
+ const subheading = computed(() => t(s.value.SUBHEADING));
82
+
83
+ const vars = computed(() => {
84
+ const v = s.value;
85
+ const spot = SPOTS[String(v.OVERLAY_POSITION || (isPromo.value ? "BOTTOM_LEFT" : "BOTTOM_LEFT")).toUpperCase()] ?? SPOTS.BOTTOM_LEFT;
86
+ const alignKey = String(v.TEXT_ALIGNMENT || "LEFT").toUpperCase();
87
+ const out = {
88
+ "--ig-cols": num(v.COLUMNS, isPromo.value ? 2 : 3),
89
+ "--ig-cols-tablet": num(v.COLUMNS_TABLET, 2),
90
+ "--ig-cols-mobile": num(v.COLUMNS_MOBILE, 1),
91
+ "--ig-gap": px(v.GAP) ?? "16px",
92
+ "--ig-gap-mobile": px(v.GAP_MOBILE) ?? px(v.GAP) ?? "12px",
93
+ "--ig-ratio": RATIOS[v.RATIO] ?? (isPromo.value ? RATIOS.LANDSCAPE : RATIOS.PORTRAIT_4_5),
94
+ "--ig-ratio-mobile": RATIOS[v.RATIO_MOBILE] ?? RATIOS[v.RATIO] ?? (isPromo.value ? RATIOS.LANDSCAPE : RATIOS.PORTRAIT_4_5),
95
+ "--ig-fit": String(v.IMAGE_FIT || "COVER").toLowerCase(),
96
+ "--ig-radius": px(v.CARD_RADIUS) ?? "0px",
97
+ "--ig-ov-y": spot[0],
98
+ "--ig-ov-x": spot[1],
99
+ "--ig-text-align": TEXT_ALIGN[alignKey] ?? "start",
100
+ "--ig-text-items": ITEMS[alignKey] ?? "flex-start",
101
+ };
102
+ if (v.OVERLAY_COLOR) out["--ig-ov-color"] = v.OVERLAY_COLOR;
103
+ if (has(v.OVERLAY_OPACITY)) out["--ig-ov-opacity-base"] = String(Number(v.OVERLAY_OPACITY) / 100);
104
+ if (v.OVERLAY_TEXT_COLOR) out["--ig-ov-text"] = v.OVERLAY_TEXT_COLOR;
105
+ if (v.CARD_BG_COLOR) out["--ig-card-bg"] = v.CARD_BG_COLOR;
106
+ if (px(v.CARD_PADDING)) out["--ig-card-pad"] = px(v.CARD_PADDING);
107
+ if (px(v.HEADING_SIZE)) out["--ig-h-fs"] = px(v.HEADING_SIZE);
108
+ if (px(v.HEADING_SIZE_MOBILE)) out["--ig-h-fs-mobile"] = px(v.HEADING_SIZE_MOBILE);
109
+ if (v.HEADING_COLOR) out["--ig-h-color"] = v.HEADING_COLOR;
110
+ if (px(v.TEXT_SIZE)) out["--ig-t-fs"] = px(v.TEXT_SIZE);
111
+ return out;
112
+ });
113
+
114
+ const rootClasses = computed(() => [
115
+ `osi-ig--${props.variant}`,
116
+ `osi-ig--hover-${String(s.value.HOVER_EFFECT || "ZOOM").toLowerCase()}`,
117
+ { "osi-ig--natural": s.value.RATIO === "NATURAL" },
118
+ ]);
119
+
120
+ function cardTag(card) {
121
+ return card.url && !card.button && !card.placeholder ? NuxtLinkComponent : "div";
122
+ }
123
+
124
+ return { s, t, shellStyle, innerStyle, align, customClass, heading, mediaUrl, isMobile, placeholders, NuxtLinkComponent, isPromo, has, num, px, VIDEO_EXT, isVideoUrl, RATIOS, SPOTS, TEXT_ALIGN, ITEMS, defaultPosition, cards, subheading, vars, rootClasses, cardTag };
125
+ }
@@ -0,0 +1,98 @@
1
+
2
+ export default function useLogic({ props }) {
3
+ const s = computed(() => props.settings ?? {});
4
+ const layout = computed(() => String(s.value.LAYOUT ?? "GRID").toUpperCase());
5
+ const isCarousel = computed(() => layout.value === "CAROUSEL");
6
+ const config = computed(() => ({ template: props.template }));
7
+
8
+ /* ---------------------------------------------------------------- variables */
9
+
10
+ const styleVars = computed(() => {
11
+ const v = s.value;
12
+ const num = (value, fallbackValue) => {
13
+ const n = Number(value);
14
+ return value === "" || value == null || !Number.isFinite(n) ? fallbackValue : n;
15
+ };
16
+ const one = props.featured;
17
+ const vars = {
18
+ // grid (FEATURED: one full-width card)
19
+ "--osi-cl-cols": String(one ? 1 : num(v.COLUMNS, 3)),
20
+ "--osi-cl-cols-tablet": String(one ? 1 : num(v.COLUMNS_TABLET, 2)),
21
+ "--osi-cl-cols-mobile": String(one ? 1 : num(v.COLUMNS_MOBILE, 2)),
22
+ "--osi-cl-gap-x": `${num(v.GAP_X, 24)}px`,
23
+ "--osi-cl-gap-y": `${num(v.GAP_Y, 32)}px`,
24
+ // bento / editorial
25
+ "--osi-cl-bento-row": `${num(v.BENTO_ROW_HEIGHT, 240)}px`,
26
+ "--osi-cl-bento-row-mobile": `${num(v.BENTO_ROW_HEIGHT_MOBILE, 180)}px`,
27
+ "--osi-cl-ed-cols": String(num(v.EDITORIAL_COLUMNS, 3)),
28
+ "--osi-cl-ed-cols-mobile": String(num(v.EDITORIAL_COLUMNS_MOBILE, 2)),
29
+ "--osi-cl-ed-offset": `${num(v.EDITORIAL_OFFSET, 64)}px`,
30
+ };
31
+ const setPx = (key, value) => {
32
+ if (value !== undefined && value !== null && value !== "") vars[key] = `${Number(value)}px`;
33
+ };
34
+ // carousel arrows
35
+ const ARROW_RADIUS = { CIRCLE: "999px", SQUARE: "0px", ROUNDED: "8px" };
36
+ setPx("--osi-cl-arrow-size", v.ARROW_SIZE);
37
+ setPx("--osi-cl-arrow-icon", v.ARROW_ICON_SIZE);
38
+ setPx("--osi-cl-arrow-offset", v.ARROW_OFFSET);
39
+ setPx("--osi-cl-arrow-border-w", v.ARROW_BORDER_WIDTH);
40
+ if (ARROW_RADIUS[v.ARROW_SHAPE]) vars["--osi-cl-arrow-radius"] = ARROW_RADIUS[v.ARROW_SHAPE];
41
+ if (v.ARROW_BG) vars["--osi-cl-arrow-bg"] = v.ARROW_BG;
42
+ if (v.ARROW_COLOR) vars["--osi-cl-arrow-color"] = v.ARROW_COLOR;
43
+ if (v.ARROW_BORDER_COLOR) vars["--osi-cl-arrow-border"] = v.ARROW_BORDER_COLOR;
44
+ if (v.ARROW_HOVER_BG) vars["--osi-cl-arrow-hover-bg"] = v.ARROW_HOVER_BG;
45
+ if (v.ARROW_HOVER_COLOR) vars["--osi-cl-arrow-hover-color"] = v.ARROW_HOVER_COLOR;
46
+ if (v.ARROW_SHADOW) vars["--osi-cl-arrow-shadow"] = "0 4px 14px -4px rgba(0,0,0,0.25)";
47
+ vars["--osi-cl-arrows-justify"] =
48
+ { LEFT: "flex-start", CENTER: "center", RIGHT: "flex-end" }[v.ARROWS_BELOW_ALIGN] ?? "center";
49
+ // carousel dots
50
+ setPx("--osi-cl-dot-size", v.DOT_SIZE);
51
+ setPx("--osi-cl-dot-active-width", v.DOT_ACTIVE_WIDTH);
52
+ setPx("--osi-cl-dot-gap", v.DOT_GAP);
53
+ setPx("--osi-cl-dots-space", v.DOTS_SPACING);
54
+ if (v.DOT_COLOR) vars["--osi-cl-dot-color"] = v.DOT_COLOR;
55
+ if (v.DOT_ACTIVE_COLOR) vars["--osi-cl-dot-active-color"] = v.DOT_ACTIVE_COLOR;
56
+ vars["--osi-cl-dots-justify"] =
57
+ { LEFT: "flex-start", CENTER: "center", RIGHT: "flex-end" }[v.DOTS_ALIGN] ?? "center";
58
+ vars["--osi-cl-dots-text-align"] =
59
+ { LEFT: "start", CENTER: "center", RIGHT: "end" }[v.DOTS_ALIGN] ?? "center";
60
+ return vars;
61
+ });
62
+
63
+ const perView = computed(() => Number(s.value.SLIDES_PER_VIEW) || 4);
64
+
65
+ const carouselOptions = computed(() => ({
66
+ perView: perView.value,
67
+ perViewTablet: Number(s.value.SLIDES_PER_VIEW_TABLET) || 3,
68
+ perViewMobile: Number(s.value.SLIDES_PER_VIEW_MOBILE) || 1.3,
69
+ gap: Number(s.value.GAP_X ?? 24),
70
+ loop: !!s.value.CAROUSEL_LOOP,
71
+ autoplay: !!(s.value.CAROUSEL_AUTOPLAY ?? s.value.AUTOPLAY),
72
+ speed: Number(s.value.CAROUSEL_SPEED) || 5,
73
+ gapMobile: s.value.GAP_X_MOBILE ?? undefined,
74
+ showArrows: s.value.SHOW_ARROWS !== false,
75
+ // The old "in the header" spot is now just above the list, on the right.
76
+ arrowPosition: String(s.value.ARROW_POSITION || "SIDES").toUpperCase(),
77
+ arrowsOnHover: String(s.value.ARROW_VISIBILITY || "ALWAYS").toUpperCase() === "HOVER",
78
+ arrowsOnMobile: !!s.value.ARROW_SHOW_MOBILE,
79
+ arrowSvg: s.value.ARROW_SVG || "",
80
+ showDots: !!s.value.SHOW_DOTS,
81
+ dotsStyle: String(s.value.DOTS_STYLE || "DOTS").toUpperCase(),
82
+ dotsDynamic: !!s.value.DOTS_DYNAMIC,
83
+ railClass: s.value.RAIL_CUSTOM_CLASS || "",
84
+ arrowClass: s.value.ARROW_CUSTOM_CLASS || "",
85
+ }));
86
+
87
+ /* Arrows "in the header": above the list, driven through the carousel. */
88
+ const carousel = ref(null);
89
+ const headerArrows = computed(
90
+ () =>
91
+ isCarousel.value &&
92
+ s.value.SHOW_ARROWS !== false &&
93
+ String(s.value.ARROW_POSITION || "SIDES").toUpperCase() === "HEADER" &&
94
+ props.items.length > perView.value
95
+ );
96
+
97
+ return { s, layout, isCarousel, config, styleVars, perView, carouselOptions, carousel, headerArrows };
98
+ }