@meeovi/layer-shared 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 (327) hide show
  1. package/README.md +0 -0
  2. package/app/components/Gallery/Gallery.vue +187 -0
  3. package/app/components/Gallery/__tests__/Gallery.spec.ts +14 -0
  4. package/app/components/Heading/Heading.vue +14 -0
  5. package/app/components/Heading/__tests__/Heading.spec.ts +14 -0
  6. package/app/components/Heading/types.ts +5 -0
  7. package/app/components/media/audioGallery.vue +70 -0
  8. package/app/components/media/dragDropUpload.vue +67 -0
  9. package/app/components/media/fullscreenMediaModal.vue +66 -0
  10. package/app/components/media/imageCard.vue +65 -0
  11. package/app/components/media/imageGallery.vue +40 -0
  12. package/app/components/media/mediaCard.vue +89 -0
  13. package/app/components/media/mediaCarousel.vue +65 -0
  14. package/app/components/media/mediaFolderSidebar.vue +72 -0
  15. package/app/components/media/mediaPlayer.vue +40 -0
  16. package/app/components/media/mediaSearchBar.vue +16 -0
  17. package/app/components/media/videoGallery.vue +77 -0
  18. package/app/components/ui/AccordionItem/AccordionItem.vue +24 -0
  19. package/app/components/ui/AccordionItem/__tests__/AccordionItem.spec.ts +14 -0
  20. package/app/components/ui/AccordionItem/types.ts +5 -0
  21. package/app/components/ui/Alert/Alert.vue +34 -0
  22. package/app/components/ui/Alert/types.ts +5 -0
  23. package/app/components/ui/Breadcrumbs/Breadcrumbs.vue +76 -0
  24. package/app/components/ui/Breadcrumbs/__tests__/Breadcrumbs.spec.ts +14 -0
  25. package/app/components/ui/Breadcrumbs/types.ts +8 -0
  26. package/app/components/ui/CartProductCard/CartProductCard.vue +66 -0
  27. package/app/components/ui/CartProductCard/types.ts +18 -0
  28. package/app/components/ui/CategoryCard/CategoryCard.vue +41 -0
  29. package/app/components/ui/CategoryCard/types.ts +9 -0
  30. package/app/components/ui/Display/Display.vue +55 -0
  31. package/app/components/ui/Display/types.ts +12 -0
  32. package/app/components/ui/Divider/Divider.vue +3 -0
  33. package/app/components/ui/Divider/__tests__/Divider.spec.tsx +10 -0
  34. package/app/components/ui/Footer.vue +92 -0
  35. package/app/components/ui/Form/FormHelperText.vue +5 -0
  36. package/app/components/ui/Form/FormLabel.vue +5 -0
  37. package/app/components/ui/Form/FormPasswordInput.vue +15 -0
  38. package/app/components/ui/Form/__tests__/FormHelperText.spec.ts +10 -0
  39. package/app/components/ui/Form/__tests__/FormLabel.spec.ts +10 -0
  40. package/app/components/ui/Hero/Hero.vue +44 -0
  41. package/app/components/ui/Hero/types.ts +10 -0
  42. package/app/components/ui/Modal/Modal.vue +19 -0
  43. package/app/components/ui/Modal/types.ts +8 -0
  44. package/app/components/ui/Motionable.vue +45 -0
  45. package/app/components/ui/NavbarBottom.vue +63 -0
  46. package/app/components/ui/NavbarTop.vue +25 -0
  47. package/app/components/ui/Overlay/Overlay.vue +14 -0
  48. package/app/components/ui/Overlay/__tests__/Overlay.spec.ts +14 -0
  49. package/app/components/ui/Overlay/types.ts +3 -0
  50. package/app/components/ui/PageBuilder.vue +39 -0
  51. package/app/components/ui/PageContainer.vue +5 -0
  52. package/app/components/ui/Pagination/Pagination.vue +151 -0
  53. package/app/components/ui/Pagination/__tests__/Pagination.spec.ts +17 -0
  54. package/app/components/ui/Pagination/types.ts +6 -0
  55. package/app/components/ui/ProductCard/ProductCard.vue +55 -0
  56. package/app/components/ui/ProductCard/__tests__/ProductCard.spec.ts +16 -0
  57. package/app/components/ui/ProductCard/types.ts +12 -0
  58. package/app/components/ui/ProductCardHorizontal/ProductCardHorizontal.vue +34 -0
  59. package/app/components/ui/ProductCardHorizontal/__tests__/ProductCardHorizontal.spec.ts +36 -0
  60. package/app/components/ui/ProductCardHorizontal/types.ts +8 -0
  61. package/app/components/ui/PurchaseCard/PurchaseCard.vue +109 -0
  62. package/app/components/ui/PurchaseCard/__tests__/PurchaseCard.spec.ts +15 -0
  63. package/app/components/ui/PurchaseCard/types.ts +5 -0
  64. package/app/components/ui/QuantitySelector/QuantitySelector.vue +69 -0
  65. package/app/components/ui/QuantitySelector/__tests__/QuantitySelector.spec.ts +15 -0
  66. package/app/components/ui/QuantitySelector/types.ts +5 -0
  67. package/app/components/ui/RadialProgress.vue +44 -0
  68. package/app/components/ui/Review/Review.vue +57 -0
  69. package/app/components/ui/Review/__tests__/Review.spec.ts +15 -0
  70. package/app/components/ui/Review/types.ts +5 -0
  71. package/app/components/ui/ScrollTop.vue +82 -0
  72. package/app/components/ui/Search.vue +54 -0
  73. package/app/components/ui/Tag/Tag.vue +38 -0
  74. package/app/components/ui/Tag/__tests__/Tag.spec.ts +10 -0
  75. package/app/components/ui/Tag/types.ts +16 -0
  76. package/app/components/ui/VsfLogo.vue +7 -0
  77. package/app/components/ui/forms/BooleanInput.vue +34 -0
  78. package/app/components/ui/forms/DateTime.vue +44 -0
  79. package/app/components/ui/forms/DirectusFormElement.vue +60 -0
  80. package/app/components/ui/forms/DynamicTableElement.vue +57 -0
  81. package/app/components/ui/forms/FileInput.vue +85 -0
  82. package/app/components/ui/forms/FormField.vue +34 -0
  83. package/app/components/ui/forms/RelationSelect.vue +63 -0
  84. package/app/components/ui/forms/RepeaterInput.vue +121 -0
  85. package/app/components/ui/forms/SelectInput.vue +65 -0
  86. package/app/components/ui/forms/TextArea.vue +59 -0
  87. package/app/components/ui/forms/TextInput.vue +42 -0
  88. package/app/components/ui/forms/TiptapEditor.vue +136 -0
  89. package/app/components/ui/forms/[collection].vue +22 -0
  90. package/app/components/ui/studio/builder.vue +57 -0
  91. package/app/components/ui/studio/document.vue +69 -0
  92. package/app/components/ui/studio/email.vue +57 -0
  93. package/app/composables/globals/uploadFiles.js +41 -0
  94. package/app/composables/globals/useAdminTable.ts +12 -0
  95. package/app/composables/globals/useCustomFetch.ts +13 -0
  96. package/app/composables/globals/useDirectusField.ts +144 -0
  97. package/app/composables/globals/useDirectusForm.ts +70 -0
  98. package/app/composables/globals/useDirectusSchema.js +9 -0
  99. package/app/composables/globals/useFileManager.ts +76 -0
  100. package/app/composables/globals/useLivePreview.ts +17 -0
  101. package/app/composables/globals/useLoading.ts +23 -0
  102. package/app/composables/globals/useNavigation.js +19 -0
  103. package/app/composables/globals/useNotifications.ts +153 -0
  104. package/app/composables/globals/usePages.js +36 -0
  105. package/app/composables/globals/useRichText.ts +33 -0
  106. package/app/composables/globals/useServerRootMixin.ts +19 -0
  107. package/app/composables/globals/useVisualEditing.ts +38 -0
  108. package/app/composables/media/useFile.ts +6 -0
  109. package/app/composables/media/useMediaCenter.ts +353 -0
  110. package/app/composables/media/useVideojs.ts +45 -0
  111. package/app/composables/registry.ts +13 -0
  112. package/app/composables/types.ts +12 -0
  113. package/app/composables/useContent.ts +13 -0
  114. package/app/composables/useDirectusRequest.ts +32 -0
  115. package/app/stores/index.ts +0 -0
  116. package/app/types/api/global-search.ts +8 -0
  117. package/app/types/blocks/block-button-group.ts +7 -0
  118. package/app/types/blocks/block-button.ts +14 -0
  119. package/app/types/blocks/block-column.ts +20 -0
  120. package/app/types/blocks/block-cta.ts +10 -0
  121. package/app/types/blocks/block-divider.ts +4 -0
  122. package/app/types/blocks/block-faq.ts +12 -0
  123. package/app/types/blocks/block-form.ts +8 -0
  124. package/app/types/blocks/block-gallery.ts +14 -0
  125. package/app/types/blocks/block-hero.ts +12 -0
  126. package/app/types/blocks/block-html.ts +4 -0
  127. package/app/types/blocks/block-logocloud.ts +14 -0
  128. package/app/types/blocks/block-quote.ts +11 -0
  129. package/app/types/blocks/block-richtext.ts +7 -0
  130. package/app/types/blocks/block-steps.ts +22 -0
  131. package/app/types/blocks/block-team.ts +6 -0
  132. package/app/types/blocks/block-testimonial.ts +14 -0
  133. package/app/types/blocks/block-video.ts +10 -0
  134. package/app/types/blocks/block.ts +49 -0
  135. package/app/types/blocks/index.ts +18 -0
  136. package/app/types/componentMap.ts +15 -0
  137. package/app/types/content/category.ts +11 -0
  138. package/app/types/content/form.ts +20 -0
  139. package/app/types/content/index.ts +6 -0
  140. package/app/types/content/page.ts +76 -0
  141. package/app/types/content/post.ts +39 -0
  142. package/app/types/content/team.ts +16 -0
  143. package/app/types/content/testimonial.ts +19 -0
  144. package/app/types/directus.d.ts +47 -0
  145. package/app/types/env.d.ts +8 -0
  146. package/app/types/help/index.ts +53 -0
  147. package/app/types/index.d.ts +9 -0
  148. package/app/types/index.ts +7 -0
  149. package/app/types/meta/analytics.ts +18 -0
  150. package/app/types/meta/config.ts +21 -0
  151. package/app/types/meta/globals.ts +30 -0
  152. package/app/types/meta/index.ts +6 -0
  153. package/app/types/meta/navigation.ts +32 -0
  154. package/app/types/meta/redirect.ts +13 -0
  155. package/app/types/meta/seo.ts +19 -0
  156. package/app/types/os/contact.ts +23 -0
  157. package/app/types/os/conversation.ts +25 -0
  158. package/app/types/os/index.ts +16 -0
  159. package/app/types/os/organization.ts +54 -0
  160. package/app/types/os/os-activity.ts +28 -0
  161. package/app/types/os/os-deal.ts +45 -0
  162. package/app/types/os/os-expense.ts +22 -0
  163. package/app/types/os/os-invoice.ts +48 -0
  164. package/app/types/os/os-item.ts +18 -0
  165. package/app/types/os/os-payment.ts +29 -0
  166. package/app/types/os/os-project.ts +47 -0
  167. package/app/types/os/os-proposal.ts +84 -0
  168. package/app/types/os/os-settings.ts +19 -0
  169. package/app/types/os/os-subscription.ts +12 -0
  170. package/app/types/os/os-task.ts +34 -0
  171. package/app/types/os/os-tax-rate.ts +13 -0
  172. package/app/types/pageComponentMap.ts +8 -0
  173. package/app/types/schema.d.ts +39 -0
  174. package/app/types/schema.ts +151 -0
  175. package/app/types/system/file.ts +46 -0
  176. package/app/types/system/folder.ts +8 -0
  177. package/app/types/system/index.ts +4 -0
  178. package/app/types/system/role.ts +21 -0
  179. package/app/types/system/user.ts +56 -0
  180. package/app/utils/Timer.js +44 -0
  181. package/app/utils/billing-address.ts +24 -0
  182. package/app/utils/color.ts +14 -0
  183. package/app/utils/currency.ts +29 -0
  184. package/app/utils/embed.ts +57 -0
  185. package/app/utils/errors.ts +9 -0
  186. package/app/utils/fieldRegistry.js +89 -0
  187. package/app/utils/fonts.ts +24 -0
  188. package/app/utils/formkit.ts +75 -0
  189. package/app/utils/icons.ts +62 -0
  190. package/app/utils/index.js +0 -0
  191. package/app/utils/links.ts +28 -0
  192. package/app/utils/lodash.ts +33 -0
  193. package/app/utils/markdown.ts +9 -0
  194. package/app/utils/math.ts +25 -0
  195. package/app/utils/navigation.ts +11 -0
  196. package/app/utils/objects.ts +11 -0
  197. package/app/utils/paths.ts +21 -0
  198. package/app/utils/relations.ts +33 -0
  199. package/app/utils/strings.ts +113 -0
  200. package/app/utils/time.ts +148 -0
  201. package/app/utils/url.ts +22 -0
  202. package/app/utils/user-name.ts +21 -0
  203. package/app/utils/video/README.md +51 -0
  204. package/app/utils/video/playlist.js +0 -0
  205. package/dist/api/global-search.d.ts +8 -0
  206. package/dist/api/global-search.js +1 -0
  207. package/dist/blocks/block-button-group.d.ts +6 -0
  208. package/dist/blocks/block-button-group.js +1 -0
  209. package/dist/blocks/block-button.d.ts +13 -0
  210. package/dist/blocks/block-button.js +1 -0
  211. package/dist/blocks/block-column.d.ts +18 -0
  212. package/dist/blocks/block-column.js +1 -0
  213. package/dist/blocks/block-cta.d.ts +11 -0
  214. package/dist/blocks/block-cta.js +1 -0
  215. package/dist/blocks/block-divider.d.ts +4 -0
  216. package/dist/blocks/block-divider.js +1 -0
  217. package/dist/blocks/block-faq.d.ts +11 -0
  218. package/dist/blocks/block-faq.js +1 -0
  219. package/dist/blocks/block-form.d.ts +7 -0
  220. package/dist/blocks/block-form.js +1 -0
  221. package/dist/blocks/block-gallery.d.ts +13 -0
  222. package/dist/blocks/block-gallery.js +1 -0
  223. package/dist/blocks/block-hero.d.ts +11 -0
  224. package/dist/blocks/block-hero.js +1 -0
  225. package/dist/blocks/block-html.d.ts +4 -0
  226. package/dist/blocks/block-html.js +1 -0
  227. package/dist/blocks/block-logocloud.d.ts +13 -0
  228. package/dist/blocks/block-logocloud.js +1 -0
  229. package/dist/blocks/block-quote.d.ts +10 -0
  230. package/dist/blocks/block-quote.js +1 -0
  231. package/dist/blocks/block-richtext.d.ts +7 -0
  232. package/dist/blocks/block-richtext.js +1 -0
  233. package/dist/blocks/block-steps.d.ts +21 -0
  234. package/dist/blocks/block-steps.js +1 -0
  235. package/dist/blocks/block-team.d.ts +6 -0
  236. package/dist/blocks/block-team.js +1 -0
  237. package/dist/blocks/block-testimonial.d.ts +13 -0
  238. package/dist/blocks/block-testimonial.js +1 -0
  239. package/dist/blocks/block-video.d.ts +9 -0
  240. package/dist/blocks/block-video.js +1 -0
  241. package/dist/blocks/block.d.ts +17 -0
  242. package/dist/blocks/block.js +1 -0
  243. package/dist/blocks/index.d.ts +18 -0
  244. package/dist/blocks/index.js +1 -0
  245. package/dist/componentMap.d.ts +6 -0
  246. package/dist/componentMap.js +8 -0
  247. package/dist/content/category.d.ts +10 -0
  248. package/dist/content/category.js +1 -0
  249. package/dist/content/form.d.ts +21 -0
  250. package/dist/content/form.js +1 -0
  251. package/dist/content/index.d.ts +6 -0
  252. package/dist/content/index.js +1 -0
  253. package/dist/content/page.d.ts +38 -0
  254. package/dist/content/page.js +1 -0
  255. package/dist/content/post.d.ts +38 -0
  256. package/dist/content/post.js +1 -0
  257. package/dist/content/team.d.ts +17 -0
  258. package/dist/content/team.js +1 -0
  259. package/dist/content/testimonial.d.ts +18 -0
  260. package/dist/content/testimonial.js +1 -0
  261. package/dist/help/index.d.ts +51 -0
  262. package/dist/help/index.js +1 -0
  263. package/dist/index.d.ts +7 -0
  264. package/dist/index.js +1 -0
  265. package/dist/meta/analytics.d.ts +21 -0
  266. package/dist/meta/analytics.js +1 -0
  267. package/dist/meta/config.d.ts +22 -0
  268. package/dist/meta/config.js +1 -0
  269. package/dist/meta/globals.d.ts +33 -0
  270. package/dist/meta/globals.js +1 -0
  271. package/dist/meta/index.d.ts +6 -0
  272. package/dist/meta/index.js +1 -0
  273. package/dist/meta/navigation.d.ts +31 -0
  274. package/dist/meta/navigation.js +1 -0
  275. package/dist/meta/redirect.d.ts +12 -0
  276. package/dist/meta/redirect.js +1 -0
  277. package/dist/meta/seo.d.ts +19 -0
  278. package/dist/meta/seo.js +1 -0
  279. package/dist/os/contact.d.ts +22 -0
  280. package/dist/os/contact.js +1 -0
  281. package/dist/os/conversation.d.ts +23 -0
  282. package/dist/os/conversation.js +1 -0
  283. package/dist/os/index.d.ts +16 -0
  284. package/dist/os/index.js +1 -0
  285. package/dist/os/organization.d.ts +51 -0
  286. package/dist/os/organization.js +1 -0
  287. package/dist/os/os-activity.d.ts +26 -0
  288. package/dist/os/os-activity.js +1 -0
  289. package/dist/os/os-deal.d.ts +42 -0
  290. package/dist/os/os-deal.js +1 -0
  291. package/dist/os/os-expense.d.ts +21 -0
  292. package/dist/os/os-expense.js +1 -0
  293. package/dist/os/os-invoice.d.ts +46 -0
  294. package/dist/os/os-invoice.js +1 -0
  295. package/dist/os/os-item.d.ts +17 -0
  296. package/dist/os/os-item.js +1 -0
  297. package/dist/os/os-payment.d.ts +27 -0
  298. package/dist/os/os-payment.js +1 -0
  299. package/dist/os/os-project.d.ts +45 -0
  300. package/dist/os/os-project.js +1 -0
  301. package/dist/os/os-proposal.d.ts +61 -0
  302. package/dist/os/os-proposal.js +1 -0
  303. package/dist/os/os-settings.d.ts +17 -0
  304. package/dist/os/os-settings.js +1 -0
  305. package/dist/os/os-subscription.d.ts +12 -0
  306. package/dist/os/os-subscription.js +1 -0
  307. package/dist/os/os-task.d.ts +32 -0
  308. package/dist/os/os-task.js +1 -0
  309. package/dist/os/os-tax-rate.d.ts +12 -0
  310. package/dist/os/os-tax-rate.js +1 -0
  311. package/dist/pageComponentMap.d.ts +2 -0
  312. package/dist/pageComponentMap.js +7 -0
  313. package/dist/schema.d.ts +78 -0
  314. package/dist/schema.js +1 -0
  315. package/dist/system/file.d.ts +47 -0
  316. package/dist/system/file.js +1 -0
  317. package/dist/system/folder.d.ts +8 -0
  318. package/dist/system/folder.js +1 -0
  319. package/dist/system/index.d.ts +4 -0
  320. package/dist/system/index.js +1 -0
  321. package/dist/system/role.d.ts +20 -0
  322. package/dist/system/role.js +1 -0
  323. package/dist/system/user.d.ts +57 -0
  324. package/dist/system/user.js +1 -0
  325. package/nuxt.config.ts +5 -0
  326. package/package.json +42 -0
  327. package/tsconfig.json +15 -0
@@ -0,0 +1,55 @@
1
+ <template>
2
+ <div class="border border-neutral-200 rounded-md hover:shadow-lg flex-auto flex-shrink-0" data-testid="product-card">
3
+ <div class="relative">
4
+ <SfLink :tag="NuxtLink" :to="`${paths.product}${slug}`">
5
+ <NuxtImg
6
+ :src="imageUrl"
7
+ :alt="imageAlt"
8
+ class="object-cover rounded-md aspect-square w-full h-full"
9
+ data-testid="image-slot"
10
+ width="190"
11
+ height="190"
12
+ :loading="lazy && !priority ? 'lazy' : undefined"
13
+ :fetchpriority="priority ? 'high' : undefined"
14
+ :preload="priority"
15
+ format="webp"
16
+ />
17
+ </SfLink>
18
+ </div>
19
+ <div class="p-2 border-t border-neutral-200 typography-text-sm">
20
+ <SfLink :tag="NuxtLink" :to="`${paths.product}${slug}`" class="no-underline" variant="secondary">
21
+ {{ name }}
22
+ </SfLink>
23
+ <div class="flex items-center pt-1">
24
+ <SfRating size="xs" :value="rating ?? 0" :max="5" />
25
+ <SfLink to="#" variant="secondary" :tag="NuxtLink" class="ml-1 no-underline">
26
+ <SfCounter size="xs">{{ ratingCount }}</SfCounter>
27
+ </SfLink>
28
+ </div>
29
+ <p class="block py-2 font-normal typography-text-xs text-neutral-700 text-justify">
30
+ {{ description }}
31
+ </p>
32
+ <span class="block pb-2 font-bold typography-text-sm" data-testid="product-card-vertical-price">
33
+ ${{ price }}
34
+ </span>
35
+ <SfButton size="sm">
36
+ <template #prefix>
37
+ <SfIconShoppingCart size="sm" />
38
+ </template>
39
+ {{ $t('addToCartShort') }}
40
+ </SfButton>
41
+ </div>
42
+ </div>
43
+ </template>
44
+
45
+ <script setup lang="ts">
46
+ import { SfLink, SfRating, SfCounter, SfButton, SfIconShoppingCart } from '@storefront-ui/vue';
47
+ import type { ProductCardProps } from '~/components/ui/ProductCard/types';
48
+
49
+ withDefaults(defineProps<ProductCardProps>(), {
50
+ lazy: true,
51
+ imageAlt: '',
52
+ });
53
+
54
+ const NuxtLink = resolveComponent('NuxtLink');
55
+ </script>
@@ -0,0 +1,16 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import ProductCard from '~/components/ui/ProductCard/ProductCard.vue';
3
+
4
+ describe('<ProductCard />', () => {
5
+ it('should render component', () => {
6
+ const { getByTestId } = mount(ProductCard, {
7
+ props: {
8
+ name: 'test',
9
+ price: 100,
10
+ imageUrl: '/images/product.webp',
11
+ },
12
+ });
13
+
14
+ expect(getByTestId('product-card'));
15
+ });
16
+ });
@@ -0,0 +1,12 @@
1
+ export type ProductCardProps = {
2
+ name: string;
3
+ imageUrl: string;
4
+ imageAlt?: string;
5
+ description?: string;
6
+ rating?: number;
7
+ ratingCount?: number;
8
+ price?: number;
9
+ slug?: string;
10
+ priority?: boolean;
11
+ lazy?: boolean;
12
+ };
@@ -0,0 +1,34 @@
1
+ <template>
2
+ <div class="flex min-w-[320px] max-w-[470px] pt-4" data-testid="product-card-horizontal">
3
+ <div class="relative overflow-hidden rounded-md w-[100px]">
4
+ <NuxtImg
5
+ class="w-full h-auto border rounded-md border-neutral-200"
6
+ :src="product.gallery[0].url"
7
+ :alt="product.gallery[0].alt ?? ''"
8
+ width="100"
9
+ height="100"
10
+ />
11
+ </div>
12
+ <div class="flex flex-col pl-4 min-w-[180px] flex-1 typography-text-base">
13
+ {{ product.name }}
14
+ <div class="my-2 sm:mb-0">
15
+ <ul class="font-normal leading-5 typography-text-xs sm:typography-text-sm text-neutral-700">
16
+ <li>
17
+ <span class="mr-1">{{ product.attributes[0].name }}:</span>
18
+ <span class="font-medium">{{ product.attributes[0].value }}</span>
19
+ </li>
20
+ <li>
21
+ <span class="mr-1">{{ product.attributes[1].name }}:</span>
22
+ <span class="font-medium">{{ product.attributes[1].value }}</span>
23
+ </li>
24
+ </ul>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </template>
29
+
30
+ <script setup lang="ts">
31
+ import type { ProductHorizontalProps } from '~/components/ui/ProductCardHorizontal/types';
32
+
33
+ defineProps<ProductHorizontalProps>();
34
+ </script>
@@ -0,0 +1,36 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import ProductCardHorizontal from '~/components/ui/ProductCardHorizontal/ProductCardHorizontal.vue';
3
+
4
+ describe('<ProductCardHorizontal />', () => {
5
+ it('should render component', () => {
6
+ const { getByTestId } = mount(ProductCardHorizontal, {
7
+ props: {
8
+ product: {
9
+ name: 'Smartwatch Fitness Tracker',
10
+ gallery: [
11
+ {
12
+ alt: 'Smartwatch Fitness Tracker',
13
+ url: '/images/watch.png',
14
+ },
15
+ ],
16
+ attributes: [
17
+ {
18
+ label: 'Size',
19
+ name: 'Size',
20
+ value: '1.9″',
21
+ valueLabel: 'value',
22
+ },
23
+ {
24
+ name: 'Color',
25
+ label: 'color',
26
+ value: 'Black',
27
+ valueLabel: 'value',
28
+ },
29
+ ],
30
+ },
31
+ },
32
+ });
33
+
34
+ expect(getByTestId('product-card-horizontal'));
35
+ });
36
+ });
@@ -0,0 +1,8 @@
1
+ import type { SfProduct } from '@vue-storefront/unified-data-model';
2
+
3
+ export type ProductHorizontalProps = {
4
+ product: Omit<
5
+ SfProduct,
6
+ 'id' | 'sku' | 'slug' | 'description' | 'price' | 'primaryImage' | 'rating' | 'variants' | 'quantityLimit'
7
+ >;
8
+ };
@@ -0,0 +1,109 @@
1
+ <template>
2
+ <section
3
+ class="p-4 xl:p-6 md:border md:border-neutral-100 md:shadow-lg md:rounded-md md:sticky md:top-20"
4
+ data-testid="purchase-card"
5
+ >
6
+ <UiTag variant="secondary" strong class="mb-4">
7
+ <SfIconSell size="sm" class="mr-1" />
8
+ <span class="mr-1">{{ $t(`sale`) }}</span>
9
+ </UiTag>
10
+ <h1 class="mb-1 font-bold typography-headline-4" data-testid="product-name">{{ product.name }}</h1>
11
+ <div class="my-1">
12
+ <span class="mr-2 text-secondary-700 font-bold font-headings text-2xl" data-testid="price">
13
+ ${{ product.price?.value.amount }}
14
+ </span>
15
+ <span class="text-base font-normal text-neutral-500 line-through">
16
+ ${{ product.price?.regularPrice.amount }}
17
+ </span>
18
+ </div>
19
+ <div class="inline-flex items-center mt-4 mb-2">
20
+ <SfRating size="xs" :value="product.rating?.average" :max="5" />
21
+ <SfCounter class="ml-1" size="xs">{{ product.rating?.count }}</SfCounter>
22
+ <SfLink href="#" variant="secondary" class="ml-2 text-xs text-neutral-500">
23
+ {{ $t('reviewsCount', { count: product.rating?.count }) }}
24
+ </SfLink>
25
+ </div>
26
+ <p class="mb-4 font-normal typography-text-sm" data-testid="product-description">
27
+ {{ product.description }}
28
+ </p>
29
+ <div class="py-4 mb-4 border-gray-200 border-y">
30
+ <UiTag class="w-full mb-4">
31
+ <SfIconShoppingCartCheckout />
32
+ {{ $t('numberInCart', { count: 1 }) }}
33
+ </UiTag>
34
+ <div class="flex flex-col md:flex-row flex-wrap gap-4">
35
+ <UiQuantitySelector :value="quantitySelectorValue" class="min-w-[145px] flex-grow flex-shrink-0 basis-0" />
36
+ <SfButton size="lg" class="flex-grow-[2] flex-shrink basis-auto whitespace-nowrap">
37
+ <template #prefix>
38
+ <SfIconShoppingCart size="sm" />
39
+ </template>
40
+ {{ $t('addToCart') }}
41
+ </SfButton>
42
+ </div>
43
+ <div class="flex justify-center mt-4 gap-x-4">
44
+ <SfButton size="sm" variant="tertiary">
45
+ <template #prefix>
46
+ <SfIconCompareArrows size="sm" />
47
+ </template>
48
+ {{ $t('compare') }}
49
+ </SfButton>
50
+ <SfButton size="sm" variant="tertiary">
51
+ <SfIconFavorite size="sm" />
52
+ {{ $t('addToList') }}
53
+ </SfButton>
54
+ </div>
55
+ </div>
56
+ <div class="flex first:mt-4">
57
+ <SfIconPackage size="sm" class="flex-shrink-0 mr-1 text-neutral-500" />
58
+ <p class="text-sm">
59
+ <i18n-t keypath="additionalInfo.shipping">
60
+ <template #addAddress>
61
+ <SfLink href="#" variant="secondary">{{ $t('additionalInfo.addAddress') }}</SfLink>
62
+ </template>
63
+ </i18n-t>
64
+ </p>
65
+ </div>
66
+ <div class="flex mt-4">
67
+ <SfIconWarehouse size="sm" class="flex-shrink-0 mr-1 text-neutral-500" />
68
+ <p class="text-sm">
69
+ <i18n-t keypath="additionalInfo.pickup">
70
+ <template #checkAvailability>
71
+ <SfLink href="#" variant="secondary">{{ $t('additionalInfo.checkAvailability') }}</SfLink>
72
+ </template>
73
+ </i18n-t>
74
+ </p>
75
+ </div>
76
+ <div class="flex mt-4">
77
+ <p class="text-sm">
78
+ <SfIconSafetyCheck size="sm" class="flex-shrink-0 mr-1 text-neutral-500" />
79
+ <i18n-t keypath="additionalInfo.returns">
80
+ <template #details>
81
+ <SfLink href="#" variant="secondary">{{ $t('additionalInfo.details') }}</SfLink>
82
+ </template>
83
+ </i18n-t>
84
+ </p>
85
+ </div>
86
+ </section>
87
+ </template>
88
+
89
+ <script setup lang="ts">
90
+ import {
91
+ SfButton,
92
+ SfCounter,
93
+ SfLink,
94
+ SfRating,
95
+ SfIconSafetyCheck,
96
+ SfIconCompareArrows,
97
+ SfIconWarehouse,
98
+ SfIconPackage,
99
+ SfIconFavorite,
100
+ SfIconSell,
101
+ SfIconShoppingCartCheckout,
102
+ SfIconShoppingCart,
103
+ } from '@storefront-ui/vue';
104
+ import type { PurchaseCardProps } from '~/components/ui/PurchaseCard/types';
105
+
106
+ defineProps<PurchaseCardProps>();
107
+
108
+ const quantitySelectorValue = ref(1);
109
+ </script>
@@ -0,0 +1,15 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import PurchaseCard from '~/components/ui/PurchaseCard/PurchaseCard.vue';
3
+ import type { SfProduct } from '@vue-storefront/unified-data-model';
4
+
5
+ describe('<PurchaseCard />', () => {
6
+ it('should render component', () => {
7
+ const wrapper = mount(PurchaseCard, {
8
+ props: {
9
+ product: {} as SfProduct,
10
+ },
11
+ });
12
+
13
+ expect(wrapper.getByTestId('purchase-card'));
14
+ });
15
+ });
@@ -0,0 +1,5 @@
1
+ import type { SfProduct } from '@vue-storefront/unified-data-model';
2
+
3
+ export type PurchaseCardProps = {
4
+ product: SfProduct;
5
+ };
@@ -0,0 +1,69 @@
1
+ <template>
2
+ <div class="inline-flex flex-col items-center" data-testid="quantity-selector">
3
+ <div class="flex border border-neutral-300 rounded-md h-full w-full">
4
+ <SfButton
5
+ variant="tertiary"
6
+ :disabled="count <= minValue"
7
+ square
8
+ class="rounded-r-none"
9
+ :aria-controls="inputId"
10
+ :aria-label="$t('quantitySelectorDecrease')"
11
+ data-testid="quantity-selector-decrease-button"
12
+ @click="dec()"
13
+ >
14
+ <SfIconRemove />
15
+ </SfButton>
16
+ <input
17
+ :id="inputId"
18
+ v-model="count"
19
+ type="number"
20
+ role="spinbutton"
21
+ :class="inputClasses"
22
+ :min="minValue"
23
+ :max="maxValue"
24
+ data-testid="quantity-selector-input"
25
+ :aria-label="$t('quantitySelector')"
26
+ @input="handleOnChange"
27
+ />
28
+ <SfButton
29
+ variant="tertiary"
30
+ :disabled="count >= maxValue"
31
+ square
32
+ class="rounded-l-none"
33
+ :aria-controls="inputId"
34
+ :aria-label="$t('quantitySelectorIncrease')"
35
+ data-testid="quantity-selector-increase-button"
36
+ @click="inc()"
37
+ >
38
+ <SfIconAdd />
39
+ </SfButton>
40
+ </div>
41
+ </div>
42
+ </template>
43
+
44
+ <script setup lang="ts">
45
+ import { clamp } from '@storefront-ui/shared';
46
+ import { SfButton, SfIconAdd, SfIconRemove, useId } from '@storefront-ui/vue';
47
+ import { useCounter } from '@vueuse/core';
48
+ import type { QuantitySelectorProps } from '~/components/ui/QuantitySelector/types';
49
+
50
+ const { value, minValue, maxValue } = withDefaults(defineProps<QuantitySelectorProps>(), {
51
+ value: 1,
52
+ minValue: 1,
53
+ maxValue: 10,
54
+ });
55
+
56
+ const inputId = useId();
57
+ const { count, inc, dec, set } = useCounter(value);
58
+
59
+ const inputClasses = computed(
60
+ () =>
61
+ 'appearance-none flex-1 mx-2 w-8 text-center bg-transparent font-medium [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-inner-spin-button]:display-none [&::-webkit-inner-spin-button]:m-0 [&::-webkit-outer-spin-button]:display-none [&::-webkit-outer-spin-button]:m-0 [-moz-appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none disabled:placeholder-disabled-900 focus-visible:outline focus-visible:outline-offset focus-visible:rounded-sm',
62
+ );
63
+
64
+ const handleOnChange = (event: Event) => {
65
+ const currentValue = (event.target as HTMLInputElement)?.value;
66
+ const nextValue = Number.parseFloat(currentValue);
67
+ set(clamp(nextValue, minValue, maxValue));
68
+ };
69
+ </script>
@@ -0,0 +1,15 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import QuantitySelector from '~/components/ui/QuantitySelector/QuantitySelector.vue';
3
+
4
+ const value = 1;
5
+
6
+ describe('<QuantitySelector />', () => {
7
+ it('should render component', () => {
8
+ const wrapper = mount(QuantitySelector, {
9
+ props: {
10
+ value,
11
+ },
12
+ });
13
+ expect(wrapper.getByTestId('quantity-selector'));
14
+ });
15
+ });
@@ -0,0 +1,5 @@
1
+ export interface QuantitySelectorProps {
2
+ value?: number;
3
+ minValue?: number;
4
+ maxValue?: number;
5
+ }
@@ -0,0 +1,44 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps({
3
+ radius: {
4
+ type: Number,
5
+ default: 50,
6
+ },
7
+ progress: {
8
+ type: Number,
9
+ default: 0,
10
+ },
11
+ stroke: {
12
+ type: Number,
13
+ default: 10,
14
+ },
15
+ });
16
+
17
+ const normalizedRadius = props.radius - props.stroke * 2;
18
+ const circumference = normalizedRadius * 2 * Math.PI;
19
+
20
+ const strokeDashoffset = computed(() => {
21
+ return circumference - ((props.progress * 100) / 100) * circumference;
22
+ });
23
+ </script>
24
+ <template>
25
+ <svg :height="radius * 2" :width="radius * 2">
26
+ <circle
27
+ stroke="currentColor"
28
+ :stroke-dasharray="circumference + ' ' + circumference"
29
+ :style="{ strokeDashoffset: strokeDashoffset }"
30
+ :stroke-width="stroke"
31
+ fill="transparent"
32
+ :r="normalizedRadius"
33
+ :cx="radius"
34
+ :cy="radius"
35
+ />
36
+ </svg>
37
+ </template>
38
+ <style>
39
+ circle {
40
+ transition: stroke-dashoffset 0.25s;
41
+ transform: rotate(-90deg);
42
+ transform-origin: 50% 50%;
43
+ }
44
+ </style>
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <article class="w-full p-4 border rounded-md" data-testid="review">
3
+ <p class="pb-2 font-medium">{{ review.title }}</p>
4
+ <header class="flex flex-col pb-2 md:flex-row md:justify-between">
5
+ <span class="flex items-center pr-2 text-xs text-neutral-500">
6
+ <SfRating :value="review.rating ?? undefined" :max="5" size="xs" class="mr-2" />
7
+ {{ $d(new Date(review.createdAt)) }}
8
+ </span>
9
+ <p class="flex items-center text-xs truncate text-primary-700">
10
+ <span class="mr-2 text-xs text-neutral-500">{{ review.reviewer }}</span>
11
+ <SfIconCheck size="xs" class="mr-1" /> {{ $t('review.verifiedPurchase') }}
12
+ </p>
13
+ </header>
14
+ <p class="pb-2 text-sm text-neutral-900">{{ truncatedContent }}</p>
15
+ <button
16
+ v-if="isButtonVisible"
17
+ type="button"
18
+ class="inline-block mb-2 text-sm font-normal border-b-2 border-black cursor-pointer w-fit hover:text-primary-700 hover:border-primary-800"
19
+ @click="isCollapsed = !isCollapsed"
20
+ >
21
+ {{ $t(isCollapsed ? 'readMore' : 'readLess') }}
22
+ </button>
23
+ <footer class="flex items-center justify-between">
24
+ <div class="text-sm text-neutral-500">
25
+ <button type="button" class="mr-6 hover:text-primary-800">
26
+ <SfIconThumbUp size="sm" class="mr-2.5" />
27
+ <SfCounter size="sm" class="text-inherit">6</SfCounter>
28
+ </button>
29
+ <button type="button" class="hover:text-primary-800">
30
+ <SfIconThumbDown size="sm" class="mr-2.5" />
31
+ <SfCounter size="sm" class="text-inherit">2</SfCounter>
32
+ </button>
33
+ </div>
34
+
35
+ <button class="px-3 py-1.5 text-neutral-500 font-medium text-sm hover:text-primary-800" type="button">
36
+ {{ $t('review.reportAbuse') }}
37
+ </button>
38
+ </footer>
39
+ </article>
40
+ </template>
41
+
42
+ <script setup lang="ts">
43
+ import { SfRating, SfIconCheck, SfIconThumbUp, SfIconThumbDown, SfCounter } from '@storefront-ui/vue';
44
+ import type { ReviewProps } from '~/components/ui/Review/types';
45
+
46
+ const props = defineProps<ReviewProps>();
47
+
48
+ const { review } = toRefs(props);
49
+ const charLimit = 400;
50
+ const isCollapsed = ref(true);
51
+ const isButtonVisible = computed(() => review.value.text?.length || 0 > charLimit);
52
+ const truncatedContent = computed(() =>
53
+ isButtonVisible.value && isCollapsed.value
54
+ ? `${review.value.text?.slice(0, Math.max(0, charLimit))}...`
55
+ : review.value.text,
56
+ );
57
+ </script>
@@ -0,0 +1,15 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import Review from '~/components/ui/Review/Review.vue';
3
+ import { mockProductReviews } from '~/composables/useProductReviews/__tests__/productReviews.mock';
4
+
5
+ describe('<Review />', () => {
6
+ it('should render component', () => {
7
+ const { getByTestId } = mount(Review, {
8
+ props: {
9
+ review: mockProductReviews[0],
10
+ },
11
+ });
12
+
13
+ expect(getByTestId('review'));
14
+ });
15
+ });
@@ -0,0 +1,5 @@
1
+ import type { SfProductReview } from '@vue-storefront/unified-data-model';
2
+
3
+ export type ReviewProps = {
4
+ review: SfProductReview;
5
+ };
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts">
2
+ import { ref, computed } from 'vue';
3
+ import { useWindowScroll } from '@vueuse/core';
4
+
5
+ const el = ref<HTMLElement | null>(null);
6
+ const { y } = useWindowScroll();
7
+
8
+ const radius = 25;
9
+ const stroke = 5;
10
+ const circumference = 2 * Math.PI * radius;
11
+ const dashArray = `${circumference} ${circumference}`;
12
+ const progress = computed(() => {
13
+ if (el.value) {
14
+ const sc = el.value.scrollTop;
15
+ const total = el.value.scrollHeight - (el.value.clientHeight || 0);
16
+ return total <= 0 ? 0 : Math.min(sc / total, 1);
17
+ }
18
+ const sc = y.value;
19
+ const doc = document.documentElement;
20
+ const total = (doc.scrollHeight || 0) - (window.innerHeight || 0);
21
+ return total <= 0 ? 0 : Math.min(sc / total, 1);
22
+ });
23
+
24
+ const dashOffset = computed(() => circumference * (1 - (progress.value ?? 0)));
25
+
26
+ function scrollToTop() {
27
+ if (el.value) el.value.scrollTo({ top: 0, behavior: 'smooth' });
28
+ else window.scrollTo({ top: 0, behavior: 'smooth' });
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <div class="flex items-center" ref="el">
34
+ <div class="relative">
35
+ <svg :width="(radius + stroke) * 2" :height="(radius + stroke) * 2" viewBox="0 0 60 60" class="text-primary/75">
36
+ <g transform="translate(30,30)">
37
+ <circle
38
+ r="25"
39
+ fill="transparent"
40
+ stroke="currentColor"
41
+ :stroke-width="stroke"
42
+ class="text-gray-200"
43
+ />
44
+ <circle
45
+ r="25"
46
+ fill="transparent"
47
+ stroke="currentColor"
48
+ :stroke-width="stroke"
49
+ stroke-linecap="round"
50
+ :stroke-dasharray="dashArray"
51
+ :stroke-dashoffset="dashOffset"
52
+ transform="rotate(-90)"
53
+ class="text-primary/75"
54
+ />
55
+ </g>
56
+ </svg>
57
+
58
+ <button
59
+ v-if="progress >= 0.95"
60
+ class="absolute inset-0 flex items-center justify-center"
61
+ @click="scrollToTop"
62
+ aria-label="Scroll to top"
63
+ >
64
+ <svg class="w-4 h-4 text-primary/75 hover:text-gray-500" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
65
+ <path d="M12 4v16" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
66
+ <path d="M18 10l-6-6-6 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
67
+ </svg>
68
+ </button>
69
+ </div>
70
+
71
+ <span
72
+ v-if="progress >= 0.95"
73
+ class="p-1.5 text-gray-700 bg-gray-300 bg-opacity-50 rounded-xl dark:bg-gray-700 dark:text-gray-200 font-display"
74
+ >
75
+ fin.
76
+ </span>
77
+ </div>
78
+ </template>
79
+
80
+ <style scoped>
81
+ .text-primary\/75 { color: theme('colors.primary', '#0ea5a0'); }
82
+ </style>
@@ -0,0 +1,54 @@
1
+ <template>
2
+ <form ref="referenceRef" role="search" class="relative" @submit.prevent="handleSubmit">
3
+ <SfInput ref="inputReference" v-model="inputModel" aria-label="Search" placeholder="Search" @focus="open">
4
+ <template #prefix>
5
+ <SfIconSearch />
6
+ </template>
7
+ <template #suffix>
8
+ <button
9
+ v-if="inputModel"
10
+ type="button"
11
+ aria-label="Reset search"
12
+ class="flex rounded-md focus-visible:outline focus-visible:outline-offset"
13
+ @click="handleReset"
14
+ >
15
+ <SfIconCancel />
16
+ </button>
17
+ </template>
18
+ </SfInput>
19
+ </form>
20
+ </template>
21
+
22
+ <script setup lang="ts">
23
+ import { SfIconCancel, SfIconSearch, SfInput, useDisclosure } from '@storefront-ui/vue';
24
+ import { unrefElement } from '@vueuse/core';
25
+
26
+ const props = defineProps<{
27
+ close?: () => boolean;
28
+ }>();
29
+
30
+ const router = useRouter();
31
+ const { open } = useDisclosure();
32
+
33
+ const inputModel = ref('');
34
+ const inputReference = ref<HTMLSpanElement>();
35
+ const handleInputFocus = () => {
36
+ const inputElement = unrefElement(inputReference)?.querySelector('input');
37
+ inputElement?.focus();
38
+ };
39
+ const handleReset = () => {
40
+ inputModel.value = '';
41
+ handleInputFocus();
42
+ };
43
+ const handleSubmit = () => {
44
+ props.close?.();
45
+ router.push({ path: paths.search, query: { search: inputModel.value } });
46
+ handleReset();
47
+ };
48
+
49
+ watch(inputModel, () => {
50
+ if (inputModel.value === '') {
51
+ handleReset();
52
+ }
53
+ });
54
+ </script>