@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.
- package/assets/css/custom.css +70 -0
- package/assets/css/remixicon.css +2782 -0
- package/assets/css/satoshi-font.css +31 -0
- package/assets/fonts/flaticon.css +463 -0
- package/assets/fonts/flaticon.eot +0 -0
- package/assets/fonts/flaticon.html +2153 -0
- package/assets/fonts/flaticon.svg +441 -0
- package/assets/fonts/flaticon.ttf +0 -0
- package/assets/fonts/flaticon.woff +0 -0
- package/assets/fonts/flaticon.woff2 +0 -0
- package/assets/fonts/remixicon.eot +0 -0
- package/assets/fonts/remixicon.svg +8230 -0
- package/assets/fonts/remixicon.ttf +0 -0
- package/assets/fonts/remixicon.woff +0 -0
- package/assets/fonts/remixicon.woff2 +0 -0
- package/assets/scss/_variables.scss +31 -0
- package/assets/scss/components/_about.scss +58 -0
- package/assets/scss/components/_authentication.scss +124 -0
- package/assets/scss/components/_backtoptop.scss +27 -0
- package/assets/scss/components/_banner.scss +396 -0
- package/assets/scss/components/_best-deals.scss +74 -0
- package/assets/scss/components/_blank.scss +40 -0
- package/assets/scss/components/_blog.scss +193 -0
- package/assets/scss/components/_cart.scss +108 -0
- package/assets/scss/components/_categories.scss +82 -0
- package/assets/scss/components/_checkout.scss +110 -0
- package/assets/scss/components/_dashboard.scss +388 -0
- package/assets/scss/components/_faq.scss +22 -0
- package/assets/scss/components/_filter-rang.scss +109 -0
- package/assets/scss/components/_footer.scss +270 -0
- package/assets/scss/components/_global.scss +550 -0
- package/assets/scss/components/_header.scss +587 -0
- package/assets/scss/components/_hurry.scss +52 -0
- package/assets/scss/components/_navbar.scss +1008 -0
- package/assets/scss/components/_offers.scss +689 -0
- package/assets/scss/components/_pagination.scss +71 -0
- package/assets/scss/components/_popup.scss +172 -0
- package/assets/scss/components/_preloader.scss +108 -0
- package/assets/scss/components/_products.scss +1147 -0
- package/assets/scss/components/_rtl.scss +806 -0
- package/assets/scss/components/_services.scss +16 -0
- package/assets/scss/components/_sidebar.scss +259 -0
- package/assets/scss/style.css +6676 -0
- package/assets/scss/style.css.map +1 -0
- package/assets/scss/style.scss +40 -0
- package/assets/webfonts/Satoshi-Bold.eot +0 -0
- package/assets/webfonts/Satoshi-Bold.woff +0 -0
- package/assets/webfonts/Satoshi-Bold.woff2 +0 -0
- package/assets/webfonts/Satoshi-Medium.eot +0 -0
- package/assets/webfonts/Satoshi-Medium.woff +0 -0
- package/assets/webfonts/Satoshi-Medium.woff2 +0 -0
- package/assets/webfonts/Satoshi-Regular.eot +0 -0
- package/assets/webfonts/Satoshi-Regular.woff +0 -0
- package/assets/webfonts/Satoshi-Regular.woff2 +0 -0
- package/components/AboutUs/AboutUsTuan.vue +25 -0
- package/components/AboutUs/Statistics.vue +42 -0
- package/components/AboutUs/SubscribeToTheNewsletter.vue +57 -0
- package/components/AddAddress/index.vue +70 -0
- package/components/BestSellers/Products.vue +1562 -0
- package/components/BestSellers/RecentlyViewed.vue +304 -0
- package/components/Cart/ProductQuantity.vue +29 -0
- package/components/Cart/index.vue +167 -0
- package/components/Categories/index.vue +305 -0
- package/components/ChangePassword/index.vue +71 -0
- package/components/Checkout/index.vue +192 -0
- package/components/Common/DashboardNavigation.vue +37 -0
- package/components/Common/PageBanner.vue +28 -0
- package/components/Common/ProductCard.vue +152 -0
- package/components/Common/Services.vue +58 -0
- package/components/Contact/ContactForm.vue +91 -0
- package/components/Contact/ContactInfo.vue +74 -0
- package/components/Dashboard/RecentOrder.vue +173 -0
- package/components/Dashboard/index.vue +79 -0
- package/components/EditAddress/index.vue +119 -0
- package/components/EditProfile/index.vue +97 -0
- package/components/FAQ/index.vue +121 -0
- package/components/FeaturedProduct/FeaturedProducts.vue +304 -0
- package/components/FeaturedProduct/Products.vue +1562 -0
- package/components/ForgotPassword/index.vue +51 -0
- package/components/Layout/BackToUp.vue +38 -0
- package/components/Layout/Copyright.vue +25 -0
- package/components/Layout/Footer.vue +183 -0
- package/components/Layout/FooterTwo.vue +165 -0
- package/components/Layout/LocationOption.vue +178 -0
- package/components/Layout/MiddleHeader.vue +229 -0
- package/components/Layout/MiddleHeaderThree.vue +204 -0
- package/components/Layout/MiddleHeaderTwo.vue +240 -0
- package/components/Layout/Navbar.vue +185 -0
- package/components/Layout/NavbarStyleFour.vue +334 -0
- package/components/Layout/NavbarStyleThree.vue +188 -0
- package/components/Layout/NavbarStyleTwo.vue +108 -0
- package/components/Layout/Preloader.vue +18 -0
- package/components/Layout/RTLSwitchBtn.vue +40 -0
- package/components/Layout/ResponsiveNavbar.vue +431 -0
- package/components/Layout/TopHeader.vue +130 -0
- package/components/Login/index.vue +94 -0
- package/components/MyAccount/index.vue +154 -0
- package/components/NewArrivals/Products.vue +1969 -0
- package/components/NewArrivals/RecentlyViewed.vue +304 -0
- package/components/OrderDetails/index.vue +77 -0
- package/components/OrderHistory/index.vue +197 -0
- package/components/PrivacyPolicy/index.vue +112 -0
- package/components/ProductDetails/ProductDetailsTab.vue +343 -0
- package/components/ProductDetails/ProductQuantity.vue +29 -0
- package/components/ProductDetails/RecentlyViewed.vue +304 -0
- package/components/ProductDetails/index.vue +268 -0
- package/components/Products/RecentlyViewed.vue +304 -0
- package/components/Products/index.vue +292 -0
- package/components/Register/index.vue +88 -0
- package/components/TermsConditions/index.vue +112 -0
- package/components/TrendingProducts/FeaturedProducts.vue +304 -0
- package/components/TrendingProducts/Products.vue +1564 -0
- package/components/Wishlist/ProductQuantity.vue +29 -0
- package/components/Wishlist/index.vue +128 -0
- package/composables/useCart.ts +149 -0
- package/composables/useCategories.ts +87 -0
- package/composables/useCheckout.ts +131 -0
- package/composables/useProducts.ts +162 -0
- package/composables/useSiteConfig.ts +236 -0
- package/composables/useTemplateSections.ts +74 -0
- package/e2e/cart.spec.ts +71 -0
- package/e2e/fixtures/mock-api.ts +166 -0
- package/e2e/homepage.spec.ts +65 -0
- package/e2e/layout.spec.ts +73 -0
- package/e2e/navigation.spec.ts +61 -0
- package/e2e/pages/cart.page.ts +44 -0
- package/e2e/pages/homepage.page.ts +46 -0
- package/e2e/playwright.config.ts +32 -0
- package/e2e/products.spec.ts +33 -0
- package/layouts/default.vue +94 -0
- package/layouts/inner.vue +70 -0
- package/nuxt.config.ts +86 -0
- package/package.json +38 -0
- package/pages/about-us.vue +12 -0
- package/pages/address.vue +10 -0
- package/pages/cart.vue +10 -0
- package/pages/categories.vue +10 -0
- package/pages/change-password.vue +10 -0
- package/pages/checkout.vue +10 -0
- package/pages/contact.vue +11 -0
- package/pages/dashboard.vue +10 -0
- package/pages/edit-address.vue +10 -0
- package/pages/edit-profile.vue +10 -0
- package/pages/faq.vue +10 -0
- package/pages/forgot-password.vue +10 -0
- package/pages/login.vue +10 -0
- package/pages/my-account.vue +10 -0
- package/pages/order-details.vue +10 -0
- package/pages/order-history.vue +10 -0
- package/pages/privacy-policy.vue +10 -0
- package/pages/product-details.vue +10 -0
- package/pages/products.vue +10 -0
- package/pages/register.vue +10 -0
- package/pages/terms-conditions.vue +10 -0
- package/pages/wishlist.vue +10 -0
- package/plugins/site-init.client.ts +24 -0
- package/plugins/vuetify.ts +18 -0
- package/types/index.ts +121 -0
- package/utils/image.ts +13 -0
- 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
|
+
};
|