@osimart/create-storefront 0.0.0-stage → 1.0.1

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.js +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.js +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 +282 -0
  200. package/nuxt/osi-sections.ts +56 -0
  201. package/nuxt.config.ts +60 -0
  202. package/package.json +44 -4
  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,163 @@
1
+ <template>
2
+ <banner pageTitle="verification code" />
3
+
4
+ <div
5
+ class="mx-10 flex flex-col items-center justify-center py-10 font-body text-base 2xl:text-xl"
6
+ >
7
+ <h2
8
+ class="text-2xl px-4 py-4 font-body s:text-4xl font-bold text-primaryColor text-center max-w-[400px] sm:max-w-[600px]"
9
+ >
10
+ We have sent a verification code to your email address
11
+ </h2>
12
+
13
+ <div class="flex gap-4 flex-col">
14
+ <div class="flex gap-2 s:gap-4 justify-center">
15
+ <input
16
+ v-for="(digit, index) in code"
17
+ :key="index"
18
+ v-model="code[index]"
19
+ ref="inputs"
20
+ type="text"
21
+ maxlength="1"
22
+ inputmode="numeric"
23
+ autocomplete="one-time-code"
24
+ :aria-label="`Digit ${index + 1}`"
25
+ class="osi-input osi-otp"
26
+ @input="handleInput(index)"
27
+ @keydown.backspace="handleBackspace(index, $event)"
28
+ :disabled="index > 0 && !code[index - 1]"
29
+ />
30
+ </div>
31
+ <div>
32
+ <p class="text-primaryColor text-center text-sm">
33
+ Didn't receive code?
34
+ <button type="button" class="osi-btn osi-btn--text osi-verify-resend" @click="resendCode">
35
+ Resend it
36
+ </button>
37
+ </p>
38
+ </div>
39
+ </div>
40
+ <button
41
+ type="button"
42
+ class="osi-btn osi-verify-submit"
43
+ :disabled="isLoading"
44
+ :aria-busy="isLoading"
45
+ @click="submitCode"
46
+ >
47
+ <span v-if="isLoading" class="osi-btn-spinner" aria-hidden="true" />
48
+ <span>Submit</span>
49
+ </button>
50
+ </div>
51
+ </template>
52
+
53
+ <script>
54
+ import { Banner as banner } from "#components";
55
+ import AuthenticationService from "../services/Authentication/AuthenticationService";
56
+ import { authBus } from "../services/AuthBus";
57
+ import { eventBus } from "../utils/eventBus";
58
+ import { useToast } from "vue-toastification";
59
+ import FadeLoader from "vue-spinner/src/FadeLoader.vue";
60
+
61
+ export default {
62
+ components: {
63
+ FadeLoader,
64
+ banner,
65
+ },
66
+ data() {
67
+ return {
68
+ code: ["", "", "", ""],
69
+ email: this.$route.query.email || "",
70
+ errorMessage: "",
71
+ isLoading: false,
72
+ formData: {
73
+ profile: {
74
+ checkpoint: 0,
75
+ },
76
+ },
77
+ };
78
+ },
79
+ mounted() {
80
+ this.$nextTick(() => {
81
+ this.$refs.inputs[0].focus();
82
+ });
83
+ },
84
+
85
+ methods: {
86
+ handleInput(index) {
87
+ this.code[index] = this.code[index].replace(/\D/g, "");
88
+
89
+ if (this.code[index] && index < this.code.length - 1) {
90
+ this.$nextTick(() => {
91
+ this.$refs.inputs[index + 1].focus();
92
+ });
93
+ }
94
+ },
95
+ handleBackspace(index, event) {
96
+ if (!this.code[index] && index > 0) {
97
+ this.$nextTick(() => {
98
+ this.$refs.inputs[index - 1].focus();
99
+ });
100
+ }
101
+ },
102
+
103
+ async resendCode() {
104
+ const toast = useToast();
105
+ this.code = ["", "", "", ""];
106
+ this.$nextTick(() => {
107
+ this.$refs.inputs[0].focus();
108
+ });
109
+ try {
110
+ const response = await AuthenticationService.resendVerificationCode(
111
+ this.email
112
+ );
113
+ toast.success("Verification code resent");
114
+ } catch (error) {
115
+ //console.log(error);
116
+ if (Array.isArray(error?.data?.code)) {
117
+ this.errorMessage = error.data.code[0];
118
+ } else {
119
+ this.errorMessage = "Something went wrong. Please try again.";
120
+ }
121
+ }
122
+ },
123
+ async submitCode() {
124
+ const toast = useToast();
125
+ this.errorMessage = "";
126
+ if (this.code.includes("")) {
127
+ toast.error("Please complete the verification code.");
128
+ return;
129
+ }
130
+ this.isLoading = true;
131
+
132
+ try {
133
+ this.$router.push({
134
+ path: "/resetpassword",
135
+ query: { email: this.email, code: this.code.join("") },
136
+ });
137
+ } catch (error) {
138
+ this.isLoading = false;
139
+ //console.log(error);
140
+ if (Array.isArray(error?.data?.code)) {
141
+ toast.error(error.data.code[0]);
142
+ this.errorMessage = error.data.code[0];
143
+ } else {
144
+ toast.error("Something went wrong. Please try again.");
145
+ }
146
+ } finally {
147
+ this.isLoading = false;
148
+ }
149
+ },
150
+ },
151
+ };
152
+ </script>
153
+
154
+ <style scoped>
155
+ .osi-verify-submit {
156
+ margin-top: 2.5rem;
157
+ --btn-tt: uppercase;
158
+ }
159
+ .osi-verify-resend {
160
+ --btn-fw: 600;
161
+ --btn-fg: currentColor;
162
+ }
163
+ </style>
@@ -0,0 +1,203 @@
1
+ <template>
2
+ <div class="min-h-[60vh] flex items-center justify-center px-5 py-16 font-body">
3
+ <div class="w-full max-w-md bg-white p-8 text-center">
4
+ <!-- CHECKING -->
5
+ <template v-if="state === 'checking'">
6
+ <div class="flex justify-center mb-6">
7
+ <div class="payment-dots" role="status" aria-label="Confirming payment">
8
+ <span></span>
9
+ <span></span>
10
+ <span></span>
11
+ </div>
12
+ </div>
13
+ <h1 class="text-lg font-semibold text-primaryColor">
14
+ Confirming your payment
15
+ </h1>
16
+ <p class="text-sm text-gray-500 mt-2">
17
+ This only takes a moment. Please don't close this page.
18
+ </p>
19
+ </template>
20
+
21
+ <!-- PAID -->
22
+ <template v-else-if="state === 'paid'">
23
+ <div class="flex justify-center mb-5">
24
+ <svg
25
+ xmlns="http://www.w3.org/2000/svg"
26
+ fill="none"
27
+ viewBox="0 0 24 24"
28
+ stroke-width="1.5"
29
+ stroke="currentColor"
30
+ class="w-14 h-14 text-primaryColor"
31
+ >
32
+ <path
33
+ stroke-linecap="round"
34
+ stroke-linejoin="round"
35
+ d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
36
+ />
37
+ </svg>
38
+ </div>
39
+ <h1 class="text-lg font-semibold text-primaryColor">Payment received</h1>
40
+ <p v-if="orderNumber" class="text-sm text-gray-500 mt-2">
41
+ Order #{{ orderNumber }} is confirmed. A receipt is on its way to your
42
+ inbox.
43
+ </p>
44
+ <p v-else class="text-sm text-gray-500 mt-2">
45
+ Your order is confirmed. A receipt is on its way to your inbox.
46
+ </p>
47
+ <NuxtLink
48
+ :to="orderLink"
49
+ class="osi-btn osi-btn--block mt-6"
50
+ >
51
+ View your order
52
+ </NuxtLink>
53
+ </template>
54
+
55
+ <!-- FAILED -->
56
+ <template v-else>
57
+ <div class="flex justify-center mb-5">
58
+ <svg
59
+ xmlns="http://www.w3.org/2000/svg"
60
+ fill="none"
61
+ viewBox="0 0 24 24"
62
+ stroke-width="1.5"
63
+ stroke="currentColor"
64
+ class="w-14 h-14 text-red-600"
65
+ >
66
+ <path
67
+ stroke-linecap="round"
68
+ stroke-linejoin="round"
69
+ d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z"
70
+ />
71
+ </svg>
72
+ </div>
73
+ <h1 class="text-lg font-semibold text-primaryColor">
74
+ Payment not completed
75
+ </h1>
76
+ <p class="text-sm text-gray-500 mt-2">
77
+ {{ reason || "We could not confirm this payment." }}
78
+ </p>
79
+ <p class="text-xs text-gray-400 mt-2">
80
+ Your bag is still here — nothing was charged.
81
+ </p>
82
+ <NuxtLink
83
+ to="/checkout"
84
+ class="osi-btn osi-btn--block mt-6"
85
+ >
86
+ Back to checkout
87
+ </NuxtLink>
88
+ </template>
89
+ </div>
90
+ </div>
91
+ </template>
92
+ <script setup>
93
+ import { ref, computed, onMounted } from "vue";
94
+ import paymentServices from "#osi/services/PaymentServices";
95
+
96
+ definePageMeta({
97
+ name: "payment-result",
98
+ });
99
+
100
+ const route = useRoute();
101
+ const { store } = useStorefront();
102
+ const { clear: clearCart } = useCart();
103
+ const storeName = computed(() => store.value?.name || "");
104
+
105
+ useHead({
106
+ title: computed(() =>
107
+ storeName.value ? `Payment | ${storeName.value}` : "Payment",
108
+ ),
109
+ meta: [{ name: "robots", content: "noindex, nofollow" }],
110
+ });
111
+
112
+ const state = ref("checking");
113
+ const orderNumber = ref(null);
114
+ const orderId = ref(null);
115
+ const reason = ref("");
116
+
117
+ const orderLink = computed(() => {
118
+ const query = { placed: "1" };
119
+ if (orderId.value) query.id = orderId.value;
120
+ return { path: "/order-detail", query };
121
+ });
122
+
123
+ onMounted(async () => {
124
+ // ?status= came from our own redirect and could have been typed by hand.
125
+ // It is decoration. The server decides.
126
+ const reference = route.query.reference || paymentServices.getReference();
127
+
128
+ if (!reference) {
129
+ state.value = "failed";
130
+ reason.value = "No payment reference.";
131
+ return;
132
+ }
133
+
134
+ try {
135
+ // /verify/ is idempotent. Poll: the buyer can beat the gateway back here
136
+ // and a pending payment settles a moment later.
137
+ for (let i = 0; i < 5; i++) {
138
+ const data = await paymentServices.verify(reference);
139
+ if (data?.order_number) orderNumber.value = data.order_number;
140
+ if (data?.reason) reason.value = data.reason;
141
+ if (data?.order_id) {
142
+ orderId.value = data.order_id;
143
+ sessionStorage.setItem("orderId", data.order_id);
144
+ }
145
+
146
+ if (data?.paid) {
147
+ state.value = "paid";
148
+ break;
149
+ }
150
+ if (i < 4) await new Promise((r) => setTimeout(r, 1500));
151
+ }
152
+ if (state.value !== "paid") state.value = "failed";
153
+ } catch (error) {
154
+ state.value = "failed";
155
+ reason.value = error?.data?.detail || "We could not confirm this payment.";
156
+ } finally {
157
+ if (state.value === "paid") {
158
+ // The one and only place a card order clears the bag. clearCart()
159
+ // drops the cart token and resets the shared state, so the drawer and
160
+ // the header badge empty with it.
161
+ paymentServices.clearReference();
162
+ clearCart();
163
+ }
164
+ }
165
+ });
166
+ </script>
167
+ <style scoped>
168
+ .payment-dots {
169
+ display: flex;
170
+ gap: 8px;
171
+ align-items: flex-end;
172
+ height: 20px; /* reserves space so the jump doesn't shift layout */
173
+ }
174
+
175
+ .payment-dots span {
176
+ width: 9px;
177
+ height: 9px;
178
+ border-radius: 50%;
179
+ background-color: currentColor;
180
+ color: theme("colors.primaryColor", #000);
181
+ animation: jump 0.9s ease-in-out infinite;
182
+ }
183
+
184
+ .payment-dots span:nth-child(2) {
185
+ animation-delay: 0.15s;
186
+ }
187
+ .payment-dots span:nth-child(3) {
188
+ animation-delay: 0.3s;
189
+ }
190
+
191
+ @keyframes jump {
192
+ 0%,
193
+ 60%,
194
+ 100% {
195
+ transform: translateY(0);
196
+ opacity: 0.5;
197
+ }
198
+ 30% {
199
+ transform: translateY(-8px);
200
+ opacity: 1;
201
+ }
202
+ }
203
+ </style>
@@ -0,0 +1,50 @@
1
+ <template>
2
+ <div>
3
+ <banner pageTitle="Return and Exchange Policy" />
4
+
5
+ <div class="flex justify-center items-center">
6
+ <div
7
+ class="w-full max-w-[1800px] flex flex-col gap-5 py-10 md:py-20 px-4 sm:px-8"
8
+ >
9
+ <template v-if="!htmlContent.return_exchange">
10
+ <div v-for="n in 3" :key="n" class="flex flex-col gap-2">
11
+ <Skeletor :height="'20px'" :width="'40%'" />
12
+ <Skeletor :height="'12px'" :width="'100%'" />
13
+ <Skeletor :height="'12px'" :width="'90%'" />
14
+ <Skeletor :height="'12px'" :width="'95%'" />
15
+ </div>
16
+ </template>
17
+
18
+ <div
19
+ v-else
20
+ v-html="htmlContent.return_exchange"
21
+ class="font-body"
22
+ ></div>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </template>
27
+
28
+ <script>
29
+ import { Banner as banner } from "#components";
30
+ import policiesServices from "../services/PoliciesServices";
31
+ import "vue-skeletor/dist/vue-skeletor.css";
32
+ import { Skeletor } from "vue-skeletor";
33
+
34
+ export default {
35
+ components: {
36
+ banner,
37
+ Skeletor,
38
+ },
39
+ data() {
40
+ return {
41
+ htmlContent: {},
42
+ };
43
+ },
44
+ async mounted() {
45
+ this.htmlContent = await policiesServices.getPolicies();
46
+ },
47
+ };
48
+ </script>
49
+
50
+ <style></style>
@@ -0,0 +1,100 @@
1
+ <script setup>
2
+ import { useRenderContext } from "#osi/composables/useRenderContext";
3
+
4
+ /**
5
+ * Search results — /search?q=…
6
+ *
7
+ * Same shape as every theme-driven route: ask the backend which template
8
+ * serves the path (SEARCH), publish the payload, render its sections — like
9
+ * Shopify, the page is sections: "Search" (SEARCH_HEADER: title and search
10
+ * box), "Search results" (MAIN_SEARCH: the Toolbar and Product grid blocks) and
11
+ * whatever the merchant puts around or between them.
12
+ *
13
+ * Fallback: when /search doesn't resolve to a SEARCH template (a backend that
14
+ * doesn't route it yet), or the template is empty (an older theme), the
15
+ * default two sections are drawn. A template with sections is trusted as is —
16
+ * hidden Search results stay hidden.
17
+ *
18
+ * The query only changes what MAIN_SEARCH fetches, so the template is rendered
19
+ * once per path, not per search.
20
+ */
21
+ const defaultSections = () => [
22
+ {
23
+ key: "search-header",
24
+ type: "SEARCH_HEADER",
25
+ settings: { DIRECTION: "VERTICAL", ALIGNMENT: "CENTER", GAP: 16, PADDING_TOP: 40, PADDING_BOTTOM: 8 },
26
+ blocks: [
27
+ { key: "search-title", type: "SEARCH_TITLE", parent_key: "", settings: { PRESET: "H2" } },
28
+ { key: "search-input", type: "SEARCH_INPUT", parent_key: "", settings: {} },
29
+ ],
30
+ },
31
+ // Toolbar + Product grid; no Card in the grid: the standard product card.
32
+ {
33
+ key: "main-search",
34
+ type: "MAIN_SEARCH",
35
+ settings: { PADDING_TOP: 24, PADDING_BOTTOM: 72 },
36
+ blocks: [
37
+ { key: "search-toolbar", type: "CLASSIFICATION_TOOLBAR", parent_key: "", settings: { TOOLBAR_MARGIN_TOP: 0 } },
38
+ { key: "search-grid", type: "PRODUCT_GRID", parent_key: "", settings: { PAGINATION: "LOAD_MORE" } },
39
+ ],
40
+ },
41
+ ];
42
+
43
+ const route = useRoute();
44
+ const { render, store } = useStorefront();
45
+ const { setRenderContext } = useRenderContext();
46
+
47
+ const { data } = await useAsyncData("render:/search", () => render("/search").catch(() => null));
48
+
49
+ const templateName = (payload) => {
50
+ const t = payload?.template;
51
+ return String((t && typeof t === "object" ? t.name : t) ?? "").toUpperCase();
52
+ };
53
+
54
+ /** Only a SEARCH payload counts — an unrouted /search comes back as NOT_FOUND. */
55
+ const resolved = computed(() => {
56
+ const name = templateName(data.value);
57
+ return data.value && (!name || name === "SEARCH") ? data.value : null;
58
+ });
59
+
60
+ const payload = computed(() => {
61
+ const sections = resolved.value?.sections ?? [];
62
+ return {
63
+ ...(resolved.value ?? {}),
64
+ template: "SEARCH",
65
+ resource: { type: "SEARCH", id: null, name: "Search", slug: "search" },
66
+ sections: sections.length ? sections : defaultSections(),
67
+ seo: resolved.value?.seo ?? {},
68
+ };
69
+ });
70
+
71
+ if (import.meta.dev && !resolved.value) {
72
+ console.warn(
73
+ "[search] render() did not resolve /search to SEARCH; using the default sections. " +
74
+ "Add match_search() to resolve_path() to fix this at the source."
75
+ );
76
+ }
77
+
78
+ setRenderContext(payload.value);
79
+ watch(payload, (value) => setRenderContext(value));
80
+
81
+ /* ------------------------------------------------------------------- SEO */
82
+
83
+ const terms = computed(() => String(route.query.q ?? "").trim());
84
+ const title = computed(() =>
85
+ [terms.value ? `Search: ${terms.value}` : "Search", store.value?.name].filter(Boolean).join(" | ")
86
+ );
87
+
88
+ useHead({
89
+ title,
90
+ titleTemplate: (value) => value ?? "",
91
+ // Result pages are endless variations of the catalog: never indexed.
92
+ meta: [{ name: "robots", content: "noindex, follow" }],
93
+ });
94
+ </script>
95
+
96
+ <template>
97
+ <div>
98
+ <SectionRenderer :sections="payload.sections" />
99
+ </div>
100
+ </template>
@@ -0,0 +1,46 @@
1
+ <template>
2
+ <div>
3
+ <banner pageTitle="Shipping Policy" />
4
+
5
+ <div class="flex justify-center items-center">
6
+ <div
7
+ class="w-full max-w-[1800px] flex flex-col gap-5 py-10 md:py-20 px-4 sm:px-8"
8
+ >
9
+ <template v-if="!htmlContent.shipping">
10
+ <div v-for="n in 3" :key="n" class="flex flex-col gap-2">
11
+ <Skeletor :height="'20px'" :width="'40%'" />
12
+ <Skeletor :height="'12px'" :width="'100%'" />
13
+ <Skeletor :height="'12px'" :width="'90%'" />
14
+ <Skeletor :height="'12px'" :width="'95%'" />
15
+ </div>
16
+ </template>
17
+
18
+ <div v-else v-html="htmlContent.shipping" class="font-body"></div>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </template>
23
+
24
+ <script>
25
+ import { Banner as banner } from "#components";
26
+ import policiesServices from "../services/PoliciesServices";
27
+ import "vue-skeletor/dist/vue-skeletor.css";
28
+ import { Skeletor } from "vue-skeletor";
29
+
30
+ export default {
31
+ components: {
32
+ banner,
33
+ Skeletor,
34
+ },
35
+ data() {
36
+ return {
37
+ htmlContent: {},
38
+ };
39
+ },
40
+ async mounted() {
41
+ this.htmlContent = await policiesServices.getPolicies();
42
+ },
43
+ };
44
+ </script>
45
+
46
+ <style></style>