@crm-market/template-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 (160) hide show
  1. package/assets/css/custom.css +70 -0
  2. package/assets/css/remixicon.css +2782 -0
  3. package/assets/css/satoshi-font.css +31 -0
  4. package/assets/fonts/flaticon.css +463 -0
  5. package/assets/fonts/flaticon.eot +0 -0
  6. package/assets/fonts/flaticon.html +2153 -0
  7. package/assets/fonts/flaticon.svg +441 -0
  8. package/assets/fonts/flaticon.ttf +0 -0
  9. package/assets/fonts/flaticon.woff +0 -0
  10. package/assets/fonts/flaticon.woff2 +0 -0
  11. package/assets/fonts/remixicon.eot +0 -0
  12. package/assets/fonts/remixicon.svg +8230 -0
  13. package/assets/fonts/remixicon.ttf +0 -0
  14. package/assets/fonts/remixicon.woff +0 -0
  15. package/assets/fonts/remixicon.woff2 +0 -0
  16. package/assets/scss/_variables.scss +31 -0
  17. package/assets/scss/components/_about.scss +58 -0
  18. package/assets/scss/components/_authentication.scss +124 -0
  19. package/assets/scss/components/_backtoptop.scss +27 -0
  20. package/assets/scss/components/_banner.scss +396 -0
  21. package/assets/scss/components/_best-deals.scss +74 -0
  22. package/assets/scss/components/_blank.scss +40 -0
  23. package/assets/scss/components/_blog.scss +193 -0
  24. package/assets/scss/components/_cart.scss +108 -0
  25. package/assets/scss/components/_categories.scss +82 -0
  26. package/assets/scss/components/_checkout.scss +110 -0
  27. package/assets/scss/components/_dashboard.scss +388 -0
  28. package/assets/scss/components/_faq.scss +22 -0
  29. package/assets/scss/components/_filter-rang.scss +109 -0
  30. package/assets/scss/components/_footer.scss +270 -0
  31. package/assets/scss/components/_global.scss +550 -0
  32. package/assets/scss/components/_header.scss +587 -0
  33. package/assets/scss/components/_hurry.scss +52 -0
  34. package/assets/scss/components/_navbar.scss +1008 -0
  35. package/assets/scss/components/_offers.scss +689 -0
  36. package/assets/scss/components/_pagination.scss +71 -0
  37. package/assets/scss/components/_popup.scss +172 -0
  38. package/assets/scss/components/_preloader.scss +108 -0
  39. package/assets/scss/components/_products.scss +1147 -0
  40. package/assets/scss/components/_rtl.scss +806 -0
  41. package/assets/scss/components/_services.scss +16 -0
  42. package/assets/scss/components/_sidebar.scss +259 -0
  43. package/assets/scss/style.css +6676 -0
  44. package/assets/scss/style.css.map +1 -0
  45. package/assets/scss/style.scss +40 -0
  46. package/assets/webfonts/Satoshi-Bold.eot +0 -0
  47. package/assets/webfonts/Satoshi-Bold.woff +0 -0
  48. package/assets/webfonts/Satoshi-Bold.woff2 +0 -0
  49. package/assets/webfonts/Satoshi-Medium.eot +0 -0
  50. package/assets/webfonts/Satoshi-Medium.woff +0 -0
  51. package/assets/webfonts/Satoshi-Medium.woff2 +0 -0
  52. package/assets/webfonts/Satoshi-Regular.eot +0 -0
  53. package/assets/webfonts/Satoshi-Regular.woff +0 -0
  54. package/assets/webfonts/Satoshi-Regular.woff2 +0 -0
  55. package/components/AboutUs/AboutUsTuan.vue +25 -0
  56. package/components/AboutUs/Statistics.vue +42 -0
  57. package/components/AboutUs/SubscribeToTheNewsletter.vue +57 -0
  58. package/components/AddAddress/index.vue +70 -0
  59. package/components/BestSellers/Products.vue +1562 -0
  60. package/components/BestSellers/RecentlyViewed.vue +304 -0
  61. package/components/Cart/ProductQuantity.vue +29 -0
  62. package/components/Cart/index.vue +167 -0
  63. package/components/Categories/index.vue +305 -0
  64. package/components/ChangePassword/index.vue +71 -0
  65. package/components/Checkout/index.vue +192 -0
  66. package/components/Common/DashboardNavigation.vue +37 -0
  67. package/components/Common/PageBanner.vue +28 -0
  68. package/components/Common/ProductCard.vue +152 -0
  69. package/components/Common/Services.vue +58 -0
  70. package/components/Contact/ContactForm.vue +91 -0
  71. package/components/Contact/ContactInfo.vue +74 -0
  72. package/components/Dashboard/RecentOrder.vue +173 -0
  73. package/components/Dashboard/index.vue +79 -0
  74. package/components/EditAddress/index.vue +119 -0
  75. package/components/EditProfile/index.vue +97 -0
  76. package/components/FAQ/index.vue +121 -0
  77. package/components/FeaturedProduct/FeaturedProducts.vue +304 -0
  78. package/components/FeaturedProduct/Products.vue +1562 -0
  79. package/components/ForgotPassword/index.vue +51 -0
  80. package/components/Layout/BackToUp.vue +38 -0
  81. package/components/Layout/Copyright.vue +25 -0
  82. package/components/Layout/Footer.vue +183 -0
  83. package/components/Layout/FooterTwo.vue +165 -0
  84. package/components/Layout/LocationOption.vue +178 -0
  85. package/components/Layout/MiddleHeader.vue +229 -0
  86. package/components/Layout/MiddleHeaderThree.vue +204 -0
  87. package/components/Layout/MiddleHeaderTwo.vue +240 -0
  88. package/components/Layout/Navbar.vue +185 -0
  89. package/components/Layout/NavbarStyleFour.vue +334 -0
  90. package/components/Layout/NavbarStyleThree.vue +188 -0
  91. package/components/Layout/NavbarStyleTwo.vue +108 -0
  92. package/components/Layout/Preloader.vue +18 -0
  93. package/components/Layout/RTLSwitchBtn.vue +40 -0
  94. package/components/Layout/ResponsiveNavbar.vue +431 -0
  95. package/components/Layout/TopHeader.vue +130 -0
  96. package/components/Login/index.vue +94 -0
  97. package/components/MyAccount/index.vue +154 -0
  98. package/components/NewArrivals/Products.vue +1969 -0
  99. package/components/NewArrivals/RecentlyViewed.vue +304 -0
  100. package/components/OrderDetails/index.vue +77 -0
  101. package/components/OrderHistory/index.vue +197 -0
  102. package/components/PrivacyPolicy/index.vue +112 -0
  103. package/components/ProductDetails/ProductDetailsTab.vue +343 -0
  104. package/components/ProductDetails/ProductQuantity.vue +29 -0
  105. package/components/ProductDetails/RecentlyViewed.vue +304 -0
  106. package/components/ProductDetails/index.vue +268 -0
  107. package/components/Products/RecentlyViewed.vue +304 -0
  108. package/components/Products/index.vue +292 -0
  109. package/components/Register/index.vue +88 -0
  110. package/components/TermsConditions/index.vue +112 -0
  111. package/components/TrendingProducts/FeaturedProducts.vue +304 -0
  112. package/components/TrendingProducts/Products.vue +1564 -0
  113. package/components/Wishlist/ProductQuantity.vue +29 -0
  114. package/components/Wishlist/index.vue +128 -0
  115. package/composables/useCart.ts +149 -0
  116. package/composables/useCategories.ts +87 -0
  117. package/composables/useCheckout.ts +131 -0
  118. package/composables/useProducts.ts +162 -0
  119. package/composables/useSiteConfig.ts +236 -0
  120. package/composables/useTemplateSections.ts +74 -0
  121. package/e2e/cart.spec.ts +71 -0
  122. package/e2e/fixtures/mock-api.ts +166 -0
  123. package/e2e/homepage.spec.ts +65 -0
  124. package/e2e/layout.spec.ts +73 -0
  125. package/e2e/navigation.spec.ts +61 -0
  126. package/e2e/pages/cart.page.ts +44 -0
  127. package/e2e/pages/homepage.page.ts +46 -0
  128. package/e2e/playwright.config.ts +32 -0
  129. package/e2e/products.spec.ts +33 -0
  130. package/layouts/default.vue +94 -0
  131. package/layouts/inner.vue +70 -0
  132. package/nuxt.config.ts +86 -0
  133. package/package.json +38 -0
  134. package/pages/about-us.vue +12 -0
  135. package/pages/address.vue +10 -0
  136. package/pages/cart.vue +10 -0
  137. package/pages/categories.vue +10 -0
  138. package/pages/change-password.vue +10 -0
  139. package/pages/checkout.vue +10 -0
  140. package/pages/contact.vue +11 -0
  141. package/pages/dashboard.vue +10 -0
  142. package/pages/edit-address.vue +10 -0
  143. package/pages/edit-profile.vue +10 -0
  144. package/pages/faq.vue +10 -0
  145. package/pages/forgot-password.vue +10 -0
  146. package/pages/login.vue +10 -0
  147. package/pages/my-account.vue +10 -0
  148. package/pages/order-details.vue +10 -0
  149. package/pages/order-history.vue +10 -0
  150. package/pages/privacy-policy.vue +10 -0
  151. package/pages/product-details.vue +10 -0
  152. package/pages/products.vue +10 -0
  153. package/pages/register.vue +10 -0
  154. package/pages/terms-conditions.vue +10 -0
  155. package/pages/wishlist.vue +10 -0
  156. package/plugins/site-init.client.ts +24 -0
  157. package/plugins/vuetify.ts +18 -0
  158. package/types/index.ts +121 -0
  159. package/utils/image.ts +13 -0
  160. package/utils/store.ts +21 -0
@@ -0,0 +1,29 @@
1
+ <template>
2
+ <div class="product-quantity">
3
+ <div class="add-to-cart-counter">
4
+ <div @click="decrement">
5
+ <input type="button" class="minusBtn" value="-" />
6
+ </div>
7
+ <input type="text" size="25" v-model="internalValue" class="count" />
8
+ <div @click="increment">
9
+ <input type="button" class="plusBtn" value="+" />
10
+ </div>
11
+ </div>
12
+ </div>
13
+ </template>
14
+
15
+ <script lang="ts" setup>
16
+ import { ref } from "vue";
17
+
18
+ const internalValue = ref(0);
19
+
20
+ const increment = () => {
21
+ internalValue.value++;
22
+ };
23
+
24
+ const decrement = () => {
25
+ if (internalValue.value > 0) {
26
+ internalValue.value--;
27
+ }
28
+ };
29
+ </script>
@@ -0,0 +1,128 @@
1
+ <template>
2
+ <div class="ptb-60">
3
+ <div class="container">
4
+ <div class="table-responsive cart-table">
5
+ <table class="table align-middle">
6
+ <thead>
7
+ <tr>
8
+ <th scope="col">Product</th>
9
+ <th scope="col">Name</th>
10
+ <th scope="col">Unit Price</th>
11
+ <th scope="col">Quantity</th>
12
+ <th scope="col">Subtotal</th>
13
+ <th scope="col">Add To Cart</th>
14
+ <th scope="col">Remove</th>
15
+ </tr>
16
+ </thead>
17
+ <tbody>
18
+ <tr>
19
+ <td>
20
+ <NuxtLink to="/product-details">
21
+ <img
22
+ src="~/assets/images/product-6.png"
23
+ class="cart-product"
24
+ alt="product"
25
+ />
26
+ </NuxtLink>
27
+ </td>
28
+ <td>
29
+ <NuxtLink to="/product-details" class="title">
30
+ Multiplatform Wireless Noise-Cancelling Gaming Headset
31
+ </NuxtLink>
32
+ </td>
33
+ <td>
34
+ <span class="price">$79</span>
35
+ </td>
36
+ <td>
37
+ <WishlistProductQuantity />
38
+ </td>
39
+ <td>
40
+ <span class="price">$79</span>
41
+ </td>
42
+ <td>
43
+ <button class="btn btn-warning text-white">Add To Cart</button>
44
+ </td>
45
+ <td>
46
+ <button class="border-0 p-0 bg-transparent remove">
47
+ <i class="ri-delete-bin-line"></i>
48
+ </button>
49
+ </td>
50
+ </tr>
51
+ <tr>
52
+ <td>
53
+ <NuxtLink to="/product-details">
54
+ <img
55
+ src="~/assets/images/product-7.png"
56
+ class="cart-product"
57
+ alt="product"
58
+ />
59
+ </NuxtLink>
60
+ </td>
61
+ <td>
62
+ <NuxtLink to="/product-details" class="title">
63
+ GPS Smartwatch with Bright Touchscreen Display
64
+ </NuxtLink>
65
+ </td>
66
+ <td>
67
+ <span class="price">$119</span>
68
+ </td>
69
+ <td>
70
+ <WishlistProductQuantity />
71
+ </td>
72
+ <td>
73
+ <span class="price">$119</span>
74
+ </td>
75
+ <td>
76
+ <button class="btn btn-warning text-white">Add To Cart</button>
77
+ </td>
78
+ <td>
79
+ <button class="border-0 p-0 bg-transparent remove">
80
+ <i class="ri-delete-bin-line"></i>
81
+ </button>
82
+ </td>
83
+ </tr>
84
+ <tr>
85
+ <td>
86
+ <NuxtLink to="/product-details">
87
+ <img
88
+ src="~/assets/images/product-8.png"
89
+ class="cart-product"
90
+ alt="product"
91
+ />
92
+ </NuxtLink>
93
+ </td>
94
+ <td>
95
+ <NuxtLink to="/product-details" class="title">
96
+ SAMSUNG 32-Inch Class QLED 4K Q60C Series Quantum HDR
97
+ </NuxtLink>
98
+ </td>
99
+ <td>
100
+ <span class="price">$397</span>
101
+ </td>
102
+ <td>
103
+ <WishlistProductQuantity />
104
+ </td>
105
+ <td>
106
+ <span class="price">$397</span>
107
+ </td>
108
+ <td>
109
+ <button class="btn btn-warning text-white">Add To Cart</button>
110
+ </td>
111
+ <td>
112
+ <button class="border-0 p-0 bg-transparent remove">
113
+ <i class="ri-delete-bin-line"></i>
114
+ </button>
115
+ </td>
116
+ </tr>
117
+ </tbody>
118
+ </table>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </template>
123
+
124
+ <script>
125
+ export default {
126
+ name: "Wishlist",
127
+ };
128
+ </script>
@@ -0,0 +1,149 @@
1
+ import { ref, computed } from 'vue';
2
+
3
+ interface CartItem {
4
+ productId: string;
5
+ name: string;
6
+ price: number;
7
+ discountPrice?: number;
8
+ quantity: number;
9
+ image?: string;
10
+ }
11
+
12
+ interface Cart {
13
+ items: CartItem[];
14
+ updatedAt: number;
15
+ }
16
+
17
+ const CART_STORAGE_KEY = 'shopping_cart';
18
+
19
+ const cart = ref<Cart>({
20
+ items: [],
21
+ updatedAt: Date.now(),
22
+ });
23
+
24
+ export const useCart = () => {
25
+ // Load cart from localStorage
26
+ const loadCart = () => {
27
+ if (process.client) {
28
+ try {
29
+ const stored = localStorage.getItem(CART_STORAGE_KEY);
30
+ if (stored) {
31
+ cart.value = JSON.parse(stored);
32
+ }
33
+ } catch (e) {
34
+ console.error('Error loading cart:', e);
35
+ }
36
+ }
37
+ };
38
+
39
+ // Save cart to localStorage
40
+ const saveCart = () => {
41
+ if (process.client) {
42
+ try {
43
+ cart.value.updatedAt = Date.now();
44
+ localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(cart.value));
45
+ } catch (e) {
46
+ console.error('Error saving cart:', e);
47
+ }
48
+ }
49
+ };
50
+
51
+ // Add item to cart
52
+ const addToCart = (item: Omit<CartItem, 'quantity'>, quantity: number = 1) => {
53
+ const existingItem = cart.value.items.find(
54
+ i => i.productId === item.productId
55
+ );
56
+
57
+ if (existingItem) {
58
+ existingItem.quantity += quantity;
59
+ } else {
60
+ cart.value.items.push({
61
+ ...item,
62
+ quantity,
63
+ });
64
+ }
65
+
66
+ saveCart();
67
+ };
68
+
69
+ // Remove item from cart
70
+ const removeFromCart = (productId: string) => {
71
+ cart.value.items = cart.value.items.filter(
72
+ i => i.productId !== productId
73
+ );
74
+ saveCart();
75
+ };
76
+
77
+ // Update item quantity
78
+ const updateQuantity = (productId: string, quantity: number) => {
79
+ const item = cart.value.items.find(i => i.productId === productId);
80
+ if (item) {
81
+ if (quantity <= 0) {
82
+ removeFromCart(productId);
83
+ } else {
84
+ item.quantity = quantity;
85
+ saveCart();
86
+ }
87
+ }
88
+ };
89
+
90
+ // Clear cart
91
+ const clearCart = () => {
92
+ cart.value.items = [];
93
+ saveCart();
94
+ };
95
+
96
+ // Check if product is in cart
97
+ const isInCart = (productId: string) => {
98
+ return cart.value.items.some(i => i.productId === productId);
99
+ };
100
+
101
+ // Get item quantity
102
+ const getItemQuantity = (productId: string) => {
103
+ const item = cart.value.items.find(i => i.productId === productId);
104
+ return item?.quantity || 0;
105
+ };
106
+
107
+ // Computed properties
108
+ const itemCount = computed(() =>
109
+ cart.value.items.reduce((sum, item) => sum + item.quantity, 0)
110
+ );
111
+
112
+ const subtotal = computed(() =>
113
+ cart.value.items.reduce((sum, item) => {
114
+ const price = item.discountPrice || item.price;
115
+ return sum + (price * item.quantity);
116
+ }, 0)
117
+ );
118
+
119
+ const totalDiscount = computed(() =>
120
+ cart.value.items.reduce((sum, item) => {
121
+ if (item.discountPrice && item.discountPrice < item.price) {
122
+ return sum + ((item.price - item.discountPrice) * item.quantity);
123
+ }
124
+ return sum;
125
+ }, 0)
126
+ );
127
+
128
+ const isEmpty = computed(() => cart.value.items.length === 0);
129
+
130
+ // Initialize cart on first use
131
+ if (process.client && cart.value.items.length === 0) {
132
+ loadCart();
133
+ }
134
+
135
+ return {
136
+ cart,
137
+ addToCart,
138
+ removeFromCart,
139
+ updateQuantity,
140
+ clearCart,
141
+ isInCart,
142
+ getItemQuantity,
143
+ loadCart,
144
+ itemCount,
145
+ subtotal,
146
+ totalDiscount,
147
+ isEmpty,
148
+ };
149
+ };
@@ -0,0 +1,87 @@
1
+ import { ref, computed } from 'vue';
2
+
3
+ interface Category {
4
+ id: string;
5
+ name: string;
6
+ slug?: string;
7
+ description?: string;
8
+ image?: string;
9
+ parentId?: string;
10
+ organizationId: string;
11
+ isActive: boolean;
12
+ order?: number;
13
+ }
14
+
15
+ const categories = ref<Category[]>([]);
16
+ const loading = ref(false);
17
+ const error = ref<string | null>(null);
18
+
19
+ export const useCategories = () => {
20
+ const config = useRuntimeConfig();
21
+ const { publicApiToken } = useSiteConfig();
22
+
23
+ // Fetch all categories
24
+ const fetchCategories = async () => {
25
+ loading.value = true;
26
+ error.value = null;
27
+
28
+ try {
29
+ const token = publicApiToken.value;
30
+ if (!token) {
31
+ throw new Error('Store token not available');
32
+ }
33
+
34
+ const response = await $fetch<{ categories: Category[] }>('/site-template/public/store/categories', {
35
+ baseURL: config.public.apiBase || 'http://localhost:3001/api',
36
+ params: { token },
37
+ });
38
+
39
+ categories.value = response.categories;
40
+ return response.categories;
41
+ } catch (e: any) {
42
+ error.value = e.message || 'Failed to load categories';
43
+ console.error('Error loading categories:', e);
44
+ throw e;
45
+ } finally {
46
+ loading.value = false;
47
+ }
48
+ };
49
+
50
+ // Get category by ID
51
+ const getCategoryById = (categoryId: string) => {
52
+ return categories.value.find(c => c.id === categoryId);
53
+ };
54
+
55
+ // Get category by slug
56
+ const getCategoryBySlug = (slug: string) => {
57
+ return categories.value.find(c => c.slug === slug);
58
+ };
59
+
60
+ // Computed properties
61
+ const activeCategories = computed(() =>
62
+ categories.value.filter(c => c.isActive)
63
+ );
64
+
65
+ const topLevelCategories = computed(() =>
66
+ activeCategories.value.filter(c => !c.parentId)
67
+ .sort((a, b) => (a.order || 0) - (b.order || 0))
68
+ );
69
+
70
+ const getSubcategories = (parentId: string) => {
71
+ return activeCategories.value
72
+ .filter(c => c.parentId === parentId)
73
+ .sort((a, b) => (a.order || 0) - (b.order || 0));
74
+ };
75
+
76
+ return {
77
+ categories,
78
+ loading,
79
+ error,
80
+ fetchCategories,
81
+ getCategoryById,
82
+ getCategoryBySlug,
83
+ activeCategories,
84
+ topLevelCategories,
85
+ getSubcategories,
86
+ };
87
+ };
@@ -0,0 +1,131 @@
1
+ import { ref } from 'vue';
2
+
3
+ interface ShippingAddress {
4
+ firstName: string;
5
+ lastName: string;
6
+ email: string;
7
+ phone: string;
8
+ address: string;
9
+ city: string;
10
+ postalCode: string;
11
+ country: string;
12
+ notes?: string;
13
+ }
14
+
15
+ interface OrderData {
16
+ items: Array<{
17
+ productId: string;
18
+ quantity: number;
19
+ price: number;
20
+ }>;
21
+ shippingAddress: ShippingAddress;
22
+ paymentMethod: string;
23
+ deliveryMethod?: string;
24
+ totalAmount: number;
25
+ organizationId: string;
26
+ }
27
+
28
+ const loading = ref(false);
29
+ const error = ref<string | null>(null);
30
+ const orderId = ref<string | null>(null);
31
+
32
+ export const useCheckout = () => {
33
+ const config = useRuntimeConfig();
34
+ const { cart, clearCart } = useCart();
35
+ const { organizationId } = useSiteConfig();
36
+
37
+ // Submit order
38
+ const submitOrder = async (
39
+ shippingAddress: ShippingAddress,
40
+ paymentMethod: string,
41
+ deliveryMethod?: string
42
+ ) => {
43
+ loading.value = true;
44
+ error.value = null;
45
+
46
+ try {
47
+ const orderData: OrderData = {
48
+ items: cart.value.items.map(item => ({
49
+ productId: item.productId,
50
+ quantity: item.quantity,
51
+ price: item.discountPrice || item.price,
52
+ })),
53
+ shippingAddress,
54
+ paymentMethod,
55
+ deliveryMethod,
56
+ totalAmount: cart.value.items.reduce(
57
+ (sum, item) => sum + (item.discountPrice || item.price) * item.quantity,
58
+ 0
59
+ ),
60
+ organizationId: organizationId.value!,
61
+ };
62
+
63
+ const response = await $fetch<{ id: string }>('/order', {
64
+ method: 'POST',
65
+ baseURL: config.public.apiBase || 'http://localhost:3001/api',
66
+ body: orderData,
67
+ });
68
+
69
+ orderId.value = response.id;
70
+ clearCart();
71
+ return response;
72
+ } catch (e: any) {
73
+ error.value = e.message || 'Failed to submit order';
74
+ console.error('Error submitting order:', e);
75
+ throw e;
76
+ } finally {
77
+ loading.value = false;
78
+ }
79
+ };
80
+
81
+ // Get order by ID
82
+ const getOrder = async (id: string) => {
83
+ loading.value = true;
84
+ error.value = null;
85
+
86
+ try {
87
+ const response = await $fetch(`/order/${id}`, {
88
+ baseURL: config.public.apiBase || 'http://localhost:3001/api',
89
+ });
90
+
91
+ return response;
92
+ } catch (e: any) {
93
+ error.value = e.message || 'Failed to load order';
94
+ console.error('Error loading order:', e);
95
+ throw e;
96
+ } finally {
97
+ loading.value = false;
98
+ }
99
+ };
100
+
101
+ // Validate checkout data
102
+ const validateCheckout = (shippingAddress: ShippingAddress) => {
103
+ const errors: string[] = [];
104
+
105
+ if (!shippingAddress.firstName) errors.push('First name is required');
106
+ if (!shippingAddress.lastName) errors.push('Last name is required');
107
+ if (!shippingAddress.email) errors.push('Email is required');
108
+ if (!shippingAddress.phone) errors.push('Phone is required');
109
+ if (!shippingAddress.address) errors.push('Address is required');
110
+ if (!shippingAddress.city) errors.push('City is required');
111
+ if (!shippingAddress.postalCode) errors.push('Postal code is required');
112
+
113
+ if (cart.value.items.length === 0) {
114
+ errors.push('Cart is empty');
115
+ }
116
+
117
+ return {
118
+ valid: errors.length === 0,
119
+ errors,
120
+ };
121
+ };
122
+
123
+ return {
124
+ loading,
125
+ error,
126
+ orderId,
127
+ submitOrder,
128
+ getOrder,
129
+ validateCheckout,
130
+ };
131
+ };
@@ -0,0 +1,162 @@
1
+ import { ref, computed } from 'vue';
2
+
3
+ interface Product {
4
+ id: string;
5
+ name: string;
6
+ price: number;
7
+ discountPrice?: number;
8
+ images: string[];
9
+ description?: string;
10
+ categoryId: string;
11
+ organizationId: string;
12
+ isActive: boolean;
13
+ stock?: number;
14
+ rating?: number;
15
+ reviews?: number;
16
+ }
17
+
18
+ interface ProductFilters {
19
+ categoryId?: string;
20
+ minPrice?: number;
21
+ maxPrice?: number;
22
+ search?: string;
23
+ sortBy?: 'price' | 'name' | 'rating' | 'newest';
24
+ sortOrder?: 'asc' | 'desc';
25
+ }
26
+
27
+ const products = ref<Product[]>([]);
28
+ const loading = ref(false);
29
+ const error = ref<string | null>(null);
30
+ const currentProduct = ref<Product | null>(null);
31
+
32
+ export const useProducts = () => {
33
+ const config = useRuntimeConfig();
34
+ const { publicApiToken } = useSiteConfig();
35
+
36
+ // Fetch all products with optional filters
37
+ const fetchProducts = async (filters?: ProductFilters) => {
38
+ loading.value = true;
39
+ error.value = null;
40
+
41
+ try {
42
+ const token = publicApiToken.value;
43
+ if (!token) {
44
+ throw new Error('Store token not available');
45
+ }
46
+
47
+ const params: any = {
48
+ token,
49
+ ...filters,
50
+ };
51
+
52
+ const response = await $fetch<{ products: Product[]; total: number }>('/site-template/public/store/products', {
53
+ baseURL: config.public.apiBase || 'http://localhost:3001/api',
54
+ params,
55
+ });
56
+
57
+ products.value = response.products;
58
+ return response.products;
59
+ } catch (e: any) {
60
+ error.value = e.message || 'Failed to load products';
61
+ console.error('Error loading products:', e);
62
+ throw e;
63
+ } finally {
64
+ loading.value = false;
65
+ }
66
+ };
67
+
68
+ // Fetch single product by ID
69
+ const fetchProduct = async (productId: string) => {
70
+ loading.value = true;
71
+ error.value = null;
72
+
73
+ try {
74
+ const token = publicApiToken.value;
75
+ if (!token) {
76
+ throw new Error('Store token not available');
77
+ }
78
+
79
+ const response = await $fetch<{ product: Product }>(`/site-template/public/store/products/${productId}`, {
80
+ baseURL: config.public.apiBase || 'http://localhost:3001/api',
81
+ params: { token },
82
+ });
83
+
84
+ currentProduct.value = response.product;
85
+ return response.product;
86
+ } catch (e: any) {
87
+ error.value = e.message || 'Failed to load product';
88
+ console.error('Error loading product:', e);
89
+ throw e;
90
+ } finally {
91
+ loading.value = false;
92
+ }
93
+ };
94
+
95
+ // Fetch products for a specific section without mutating global state
96
+ const fetchProductsForSection = async (filters?: ProductFilters & { limit?: number; offset?: number }): Promise<Product[]> => {
97
+ try {
98
+ const token = publicApiToken.value;
99
+ if (!token) return [];
100
+
101
+ const params: any = {
102
+ token,
103
+ ...filters,
104
+ };
105
+
106
+ const response = await $fetch<{ products: Product[]; total: number }>('/site-template/public/store/products', {
107
+ baseURL: config.public.apiBase || 'http://localhost:3001/api',
108
+ params,
109
+ });
110
+
111
+ return response.products;
112
+ } catch (e: any) {
113
+ console.error('Error loading products for section:', e);
114
+ return [];
115
+ }
116
+ };
117
+
118
+ // Get products by category
119
+ const getProductsByCategory = async (categoryId: string) => {
120
+ return fetchProducts({ categoryId });
121
+ };
122
+
123
+ // Search products
124
+ const searchProducts = async (query: string) => {
125
+ return fetchProducts({ search: query });
126
+ };
127
+
128
+ // Computed properties
129
+ const activeProducts = computed(() =>
130
+ products.value.filter(p => p.isActive)
131
+ );
132
+
133
+ const discountedProducts = computed(() =>
134
+ activeProducts.value.filter(p => p.discountPrice && p.discountPrice < p.price)
135
+ );
136
+
137
+ const bestRatedProducts = computed(() =>
138
+ [...activeProducts.value]
139
+ .sort((a, b) => (b.rating || 0) - (a.rating || 0))
140
+ .slice(0, 10)
141
+ );
142
+
143
+ const newestProducts = computed(() =>
144
+ [...activeProducts.value].slice(0, 10)
145
+ );
146
+
147
+ return {
148
+ products,
149
+ currentProduct,
150
+ loading,
151
+ error,
152
+ fetchProducts,
153
+ fetchProduct,
154
+ fetchProductsForSection,
155
+ getProductsByCategory,
156
+ searchProducts,
157
+ activeProducts,
158
+ discountedProducts,
159
+ bestRatedProducts,
160
+ newestProducts,
161
+ };
162
+ };