@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,452 @@
1
+ <template>
2
+ <div
3
+ v-if="isVisible"
4
+ class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black bg-opacity-50"
5
+ @click.self="closeModal"
6
+ >
7
+ <div
8
+ class="osi-am-panel w-full max-w-lg overflow-hidden animate-fade-in-up"
9
+ role="dialog"
10
+ aria-modal="true"
11
+ >
12
+ <div class="p-6">
13
+ <div class="flex justify-between items-center mb-6">
14
+ <h2 class="text-xl font-semibold font-body">
15
+ {{ isEditing ? "Edit Address" : "Add New Address" }}
16
+ </h2>
17
+ <button type="button" class="osi-am-close" aria-label="Close" @click="closeModal">
18
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
19
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
20
+ </svg>
21
+ </button>
22
+ </div>
23
+
24
+ <form novalidate class="osi-am-form font-body" @submit.prevent="submitForm">
25
+ <!-- Country -->
26
+ <OsiField label="Country" required :floated="!!selectedCountry">
27
+ <template #control="{ id, inputClass, placeholderText }">
28
+ <div class="osi-am-select">
29
+ <button
30
+ :id="id"
31
+ type="button"
32
+ :class="[inputClass, 'osi-select-trigger']"
33
+ :aria-expanded="isCountryOpen"
34
+ @click="toggleCountry"
35
+ >
36
+ <span class="osi-am-trigger-text">{{ selectedCountry?.country_name || placeholderText }}</span>
37
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="osi-am-chevron" :class="{ 'is-open': isCountryOpen }" aria-hidden="true">
38
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
39
+ </svg>
40
+ </button>
41
+ <ul v-show="isCountryOpen" class="osi-am-menu" role="listbox">
42
+ <li
43
+ v-for="(country, index) in countries"
44
+ :key="index"
45
+ role="option"
46
+ class="osi-am-option"
47
+ :class="{ 'is-selected': selectedCountry?.id === country.id }"
48
+ :aria-selected="selectedCountry?.id === country.id"
49
+ @click="selectCountry(country)"
50
+ >
51
+ {{ country?.country_name }}
52
+ </li>
53
+ </ul>
54
+ </div>
55
+ </template>
56
+ </OsiField>
57
+
58
+ <!-- Zone -->
59
+ <OsiField
60
+ v-if="selectedCountry && selectedCountry.shipping_zones && selectedCountry.shipping_zones.length > 0"
61
+ label="Zone"
62
+ required
63
+ :floated="!!selectedZone"
64
+ >
65
+ <template #control="{ id, inputClass, placeholderText }">
66
+ <div class="osi-am-select">
67
+ <button
68
+ :id="id"
69
+ type="button"
70
+ :class="[inputClass, 'osi-select-trigger']"
71
+ :aria-expanded="isZoneOpen"
72
+ @click="toggleZone"
73
+ >
74
+ <span class="osi-am-trigger-text">{{ selectedZone?.name || placeholderText }}</span>
75
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="osi-am-chevron" :class="{ 'is-open': isZoneOpen }" aria-hidden="true">
76
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
77
+ </svg>
78
+ </button>
79
+ <ul v-show="isZoneOpen" class="osi-am-menu" role="listbox">
80
+ <li
81
+ v-for="(zone, index) in zones"
82
+ :key="index"
83
+ role="option"
84
+ class="osi-am-option"
85
+ :class="{ 'is-selected': selectedZone && selectedZone.id === zone.id }"
86
+ :aria-selected="!!(selectedZone && selectedZone.id === zone.id)"
87
+ @click="selectZone(zone)"
88
+ >
89
+ {{ zone.name }}
90
+ </li>
91
+ </ul>
92
+ </div>
93
+ </template>
94
+ </OsiField>
95
+
96
+ <OsiField
97
+ v-model="formData.address.address"
98
+ id="address_modal"
99
+ as="textarea"
100
+ rows="3"
101
+ label="Address"
102
+ required
103
+ autocomplete="street-address"
104
+ />
105
+
106
+ <OsiField
107
+ v-model="formData.address.postal_code"
108
+ id="postcode_modal"
109
+ label="Post Code/Zip Code"
110
+ inputmode="numeric"
111
+ autocomplete="postal-code"
112
+ />
113
+
114
+ <div class="osi-am-actions">
115
+ <button type="button" class="osi-btn osi-btn--secondary" @click="closeModal">
116
+ Cancel
117
+ </button>
118
+ <button
119
+ type="submit"
120
+ class="osi-btn"
121
+ :disabled="isLoading"
122
+ :aria-busy="isLoading"
123
+ >
124
+ <span v-if="isLoading" class="osi-btn-spinner" aria-hidden="true" />
125
+ <span>{{ isEditing ? "Save Changes" : "Add Address" }}</span>
126
+ </button>
127
+ </div>
128
+ </form>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </template>
133
+
134
+ <script setup>
135
+ import { ref, computed, watch, reactive } from 'vue';
136
+ import { OsiField } from '#components';
137
+ import UserServices from '../../../services/UserServices';
138
+ import { useToast } from 'vue-toastification';
139
+
140
+ // Props
141
+ const props = defineProps({
142
+ isVisible: {
143
+ type: Boolean,
144
+ required: true,
145
+ },
146
+ countries: {
147
+ type: Array,
148
+ default: () => [],
149
+ },
150
+ addressToEdit: {
151
+ type: Object,
152
+ default: null,
153
+ },
154
+ });
155
+
156
+ // Emits
157
+ const emit = defineEmits(['close', 'address-added']);
158
+
159
+ // Toast
160
+ const toast = useToast();
161
+
162
+ // Reactive state
163
+ const isLoading = ref(false);
164
+ const isCountryOpen = ref(false);
165
+ const isZoneOpen = ref(false);
166
+ const selectedCountry = ref(null);
167
+ const selectedZone = ref(null);
168
+ const zones = ref([]);
169
+ const formData = reactive({
170
+ address: {
171
+ country: '',
172
+ zone: '',
173
+ address: '',
174
+ postal_code: '',
175
+ },
176
+ });
177
+
178
+ // Computed
179
+ const isEditing = computed(() => !!props.addressToEdit);
180
+
181
+ // Watchers
182
+ watch(() => props.isVisible, (val) => {
183
+ if (val) {
184
+ if (isEditing.value) {
185
+ populateForm();
186
+ } else {
187
+ resetForm();
188
+ }
189
+ }
190
+ });
191
+
192
+ watch(() => props.addressToEdit, (val) => {
193
+ if (props.isVisible) {
194
+ if (val) populateForm();
195
+ else resetForm();
196
+ }
197
+ });
198
+
199
+ // Methods
200
+ const closeModal = () => {
201
+ emit('close');
202
+ };
203
+
204
+ const resetForm = () => {
205
+ formData.address = {
206
+ address: '',
207
+ country: '',
208
+ zone: '',
209
+ postal_code: '',
210
+ };
211
+ selectedCountry.value = null;
212
+ selectedZone.value = null;
213
+ zones.value = [];
214
+ isCountryOpen.value = false;
215
+ isZoneOpen.value = false;
216
+ };
217
+
218
+ const populateForm = () => {
219
+ const address = props.addressToEdit;
220
+ formData.address = {
221
+ country: address.country,
222
+ zone: address.zone,
223
+ address: address.address,
224
+ postal_code: address.postal_code,
225
+ };
226
+
227
+ // Set references for UI
228
+ selectedCountry.value = props.countries.find(c => c.id === address.country);
229
+ if (selectedCountry.value) {
230
+ zones.value = selectedCountry.value.shipping_zones || [];
231
+ if (address.zone) {
232
+ selectedZone.value = zones.value.find(z => z.id === address.zone);
233
+ } else {
234
+ selectedZone.value = null;
235
+ }
236
+ } else {
237
+ zones.value = [];
238
+ selectedZone.value = null;
239
+ }
240
+ };
241
+
242
+ const toggleCountry = () => {
243
+ isCountryOpen.value = !isCountryOpen.value;
244
+ isZoneOpen.value = false;
245
+ };
246
+
247
+ const selectCountry = (country) => {
248
+ selectedCountry.value = country;
249
+ formData.address.country = country.id;
250
+ isCountryOpen.value = false;
251
+
252
+ // Handle zones
253
+ if (country.shipping_zones && country.shipping_zones.length > 0) {
254
+ zones.value = country.shipping_zones;
255
+ selectedZone.value = null;
256
+ formData.address.zone = '';
257
+ } else {
258
+ zones.value = [];
259
+ selectedZone.value = null;
260
+ formData.address.zone = null;
261
+ }
262
+ };
263
+
264
+ const toggleZone = () => {
265
+ isZoneOpen.value = !isZoneOpen.value;
266
+ isCountryOpen.value = false;
267
+ };
268
+
269
+ const selectZone = (zone) => {
270
+ selectedZone.value = zone;
271
+ formData.address.zone = zone.id;
272
+ isZoneOpen.value = false;
273
+ };
274
+
275
+ const submitForm = async () => {
276
+ if (isEditing.value) {
277
+ await updateAddress();
278
+ } else {
279
+ await addAddress();
280
+ }
281
+ };
282
+
283
+ const addAddress = async () => {
284
+ // Basic validation
285
+ if (!formData.address.country) {
286
+ toast.error('Please select a country');
287
+ return;
288
+ }
289
+ if (!formData.address.address) {
290
+ toast.error('Please enter an address');
291
+ return;
292
+ }
293
+ if (zones.value.length > 0 && !formData.address.zone) {
294
+ toast.error('Please select a zone');
295
+ return;
296
+ }
297
+
298
+ try {
299
+ isLoading.value = true;
300
+ await UserServices.addAddress(formData.address);
301
+ toast.success('Address added successfully');
302
+ emit('address-added');
303
+ closeModal();
304
+ } catch (error) {
305
+ console.error('Error adding address:', error);
306
+ toast.error('Failed to add address');
307
+ } finally {
308
+ isLoading.value = false;
309
+ }
310
+ };
311
+
312
+ const updateAddress = async () => {
313
+ const original = props.addressToEdit;
314
+ const current = formData.address;
315
+ const updatedData = {};
316
+
317
+ // Diff logic
318
+ if (current.address !== original.address) updatedData.address = current.address;
319
+ if (current.postal_code !== original.postal_code) updatedData.postal_code = current.postal_code;
320
+
321
+ // Country changed
322
+ if (current.country !== original.country) {
323
+ updatedData.country = current.country;
324
+ const countryObj = props.countries.find(c => c.id === current.country);
325
+ if (!countryObj?.shipping_zones?.length) {
326
+ updatedData.zone = null;
327
+ } else {
328
+ if (current.zone) updatedData.zone = current.zone;
329
+ }
330
+ } else {
331
+ // Country same, check zone
332
+ if (current.zone !== original.zone) {
333
+ updatedData.zone = current.zone;
334
+ }
335
+ }
336
+
337
+ if (Object.keys(updatedData).length === 0) {
338
+ closeModal();
339
+ return;
340
+ }
341
+
342
+ try {
343
+ isLoading.value = true;
344
+ await UserServices.editAddress(original.id, updatedData);
345
+ toast.success('Address updated successfully');
346
+ emit('address-added');
347
+ closeModal();
348
+ } catch(error) {
349
+ console.error('Error updating address:', error);
350
+ toast.error('Failed to update address');
351
+ } finally {
352
+ isLoading.value = false;
353
+ }
354
+ };
355
+ </script>
356
+
357
+ <style scoped>
358
+ @keyframes fadeInUp {
359
+ from {
360
+ opacity: 0;
361
+ transform: translate3d(0, 20px, 0);
362
+ }
363
+ to {
364
+ opacity: 1;
365
+ transform: translate3d(0, 0, 0);
366
+ }
367
+ }
368
+
369
+ .animate-fade-in-up {
370
+ animation: fadeInUp 0.3s ease-out forwards;
371
+ }
372
+
373
+ .osi-am-panel {
374
+ --osi-field-chip: var(--osi-bg, #ffffff);
375
+ background: var(--osi-bg, #ffffff);
376
+ color: var(--osi-text, inherit);
377
+ border-radius: calc(var(--osi-button-radius, 4px) * 2);
378
+ box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.35);
379
+ }
380
+ .osi-am-close {
381
+ display: flex;
382
+ padding: 0;
383
+ border: 0;
384
+ background: transparent;
385
+ color: inherit;
386
+ opacity: 0.5;
387
+ cursor: pointer;
388
+ transition: opacity 0.2s ease;
389
+ }
390
+ .osi-am-close:hover {
391
+ opacity: 1;
392
+ }
393
+ .osi-am-form {
394
+ display: flex;
395
+ flex-direction: column;
396
+ gap: var(--osi-input-gap, 1rem);
397
+ }
398
+ .osi-am-select {
399
+ position: relative;
400
+ }
401
+ .osi-am-trigger-text {
402
+ min-width: 0;
403
+ overflow: hidden;
404
+ text-overflow: ellipsis;
405
+ white-space: nowrap;
406
+ }
407
+ .osi-am-chevron {
408
+ flex: none;
409
+ width: 1rem;
410
+ height: 1rem;
411
+ opacity: 0.5;
412
+ transition: transform 0.2s ease;
413
+ }
414
+ .osi-am-chevron.is-open {
415
+ transform: rotate(180deg);
416
+ }
417
+ .osi-am-menu {
418
+ position: absolute;
419
+ top: calc(100% + 4px);
420
+ left: 0;
421
+ right: 0;
422
+ z-index: 50;
423
+ max-height: 15rem;
424
+ margin: 0;
425
+ padding: 0.25rem 0;
426
+ overflow-y: auto;
427
+ list-style: none;
428
+ background: var(--osi-bg, #ffffff);
429
+ color: var(--osi-text, inherit);
430
+ border: var(--osi-input-border-w, 1px) solid var(--osi-input-border-color, var(--osi-border, #e5e5e5));
431
+ border-radius: var(--osi-input-radius, 4px);
432
+ box-shadow: 0 10px 24px -8px rgba(0, 0, 0, 0.2);
433
+ }
434
+ .osi-am-option {
435
+ padding: 0.5rem 0.75rem;
436
+ cursor: pointer;
437
+ font-size: 0.875rem;
438
+ }
439
+ .osi-am-option:hover {
440
+ background: color-mix(in srgb, currentColor 6%, transparent);
441
+ }
442
+ .osi-am-option.is-selected {
443
+ background: color-mix(in srgb, currentColor 10%, transparent);
444
+ font-weight: 500;
445
+ }
446
+ .osi-am-actions {
447
+ display: flex;
448
+ justify-content: flex-end;
449
+ gap: 0.75rem;
450
+ padding-top: 0.5rem;
451
+ }
452
+ </style>
@@ -0,0 +1,137 @@
1
+ <template>
2
+ <div
3
+ class="w-full xlg1:w-[70%] h-fit border border-gray-100 shadow-md p-5 bg-gray-50 font-body"
4
+ >
5
+ <div v-if="wishlistItems && wishlistItems.length > 0">
6
+ <div
7
+ class="bg-gray-50 grid grid-cols-1 s:grid-cols-2 l:grid-cols-3 gap-3"
8
+ >
9
+ <div
10
+ v-for="item in wishlistItems"
11
+ :key="item?.product?.id"
12
+ class="border border-primaryColor border-opacity-10"
13
+ >
14
+ <div class="relative">
15
+ <router-link
16
+ :to="`/${item?.product?.categories[0].category.slugified_name}/${item?.product?.slugified_name}/`"
17
+ >
18
+ <img
19
+ :src="mediaUrl(item?.product?.main_image?.path)"
20
+ class="w-full h-[350px] object-cover"
21
+ />
22
+ </router-link>
23
+
24
+ <button
25
+ type="button"
26
+ class="osi-btn osi-btn--icon absolute top-3 right-3 w-[40px] h-[40px] removeWishlistButton"
27
+ style="--btn-pad-y: 0px"
28
+ aria-label="Remove from wishlist"
29
+ :data-product-id="item.product.id"
30
+ @click="removeFromWishlist(item.product.id)"
31
+ >
32
+ <svg
33
+ xmlns="http://www.w3.org/2000/svg"
34
+ fill="currentColor"
35
+ viewBox="0 0 24 24"
36
+ stroke-width="1.5"
37
+ stroke="currentColor"
38
+ class="size-5"
39
+ >
40
+ <path
41
+ stroke-linecap="round"
42
+ stroke-linejoin="round"
43
+ d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
44
+ />
45
+ </svg>
46
+ </button>
47
+ </div>
48
+
49
+ <router-link :to="`/${item.product.categories[0].category.slugified_name}/${item.product.slugified_name}`">
50
+ <div class="w-full flex flex-col justify-center h-[60px] p-3">
51
+ <p class="text-primaryColor text-sm">{{ item?.product?.name }}</p>
52
+ <!-- <p class="text-gray-500 text-sm">${{ item?.product?.price }}</p> -->
53
+ </div>
54
+ </router-link>
55
+ </div>
56
+ </div>
57
+ </div>
58
+ <div v-else>
59
+ <div class="w-full flex flex-col justify-center items-center gap-5">
60
+ <svg
61
+ xmlns="http://www.w3.org/2000/svg"
62
+ fill="none"
63
+ viewBox="0 0 24 24"
64
+ stroke="currentColor"
65
+ stroke-width="1.5"
66
+ class="w-[50%] s:w-[200px] text-primaryColor opacity-10"
67
+ >
68
+ <path
69
+ stroke-linecap="round"
70
+ stroke-linejoin="round"
71
+ d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
72
+ />
73
+ </svg>
74
+
75
+ <p class="text-primaryColor font-body text-xl s:text-2xl">
76
+ Wishlist is empty
77
+ </p>
78
+ <div class="text-center text-gray-500">
79
+ <p class="font-body text-sm">
80
+ You don't have any products in your wishlist yet.
81
+ </p>
82
+ <p class="font-body text-sm">
83
+ You will find many interesting products on the "Shopping" page.
84
+ </p>
85
+ </div>
86
+ <router-link to="/" class="osi-btn mb-5">Continue Shopping</router-link>
87
+ </div>
88
+ </div>
89
+ </div>
90
+ </template>
91
+
92
+ <script setup>
93
+ import WishlistServices from "../../../services/WishlistServices";
94
+ import { useToast } from "vue-toastification";
95
+ import { useQueryClient } from "@tanstack/vue-query";
96
+
97
+ // props
98
+ const props = defineProps({
99
+ wishlistItems: {
100
+ type: Array,
101
+ required: true,
102
+ },
103
+ });
104
+
105
+ // emits
106
+ const emit = defineEmits(["update-wishlist"]);
107
+
108
+ // base URL
109
+ const { mediaUrl } = useMediaUrl();
110
+
111
+ // query client + toast
112
+ const queryClient = useQueryClient();
113
+ const toast = useToast();
114
+
115
+ // remove function
116
+ async function removeFromWishlist(productId) {
117
+ try {
118
+ await WishlistServices.removeFromWishlist(productId);
119
+ toast.success("Product removed from wishlist");
120
+
121
+ // Use props.wishlistItems instead of wishlistItems
122
+ const updatedWishlist = props.wishlistItems.filter(
123
+ (item) => item.product.id !== productId
124
+ );
125
+ emit("update-wishlist", updatedWishlist);
126
+
127
+ await queryClient.invalidateQueries({ queryKey: ["section1"] });
128
+ await queryClient.invalidateQueries({ queryKey: ["section2"] });
129
+ } catch (error) {
130
+ toast.error("Something went wrong");
131
+ console.error("Error removing from wishlist:", error);
132
+ }
133
+ }
134
+ </script>
135
+ <style scoped>
136
+ /* Add custom styles here if needed */
137
+ </style>