@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,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>
|