feat(storefront): enhance cart and wishlist handling with cookie-based products support
Introduced `useExactProducts` composable to fetch precise product details for guest cart and wishlist items. Improved cookie-based cart and wishlist fallback handling for unauthenticated users. Updated related components and composables for better synchronization and type safety. - Added `useExactProducts` composable leveraging the `GET_EXACT_PRODUCTS` query. - Enhanced `wishlist.vue` and `cart.vue` for reactive updates on guest state changes. - Improved product synchronization logic in `useOrderSync` and `useWishlistSync`. - Updated translations and fixed minor typos in localization files. Improves user experience by ensuring consistent product details, even for guests. No breaking changes.
This commit is contained in:
parent
398e11d748
commit
9bf600845a
15 changed files with 202 additions and 63 deletions
|
|
@ -173,7 +173,7 @@ const isProductInCart = computed(() => {
|
|||
);
|
||||
} else {
|
||||
return (cookieCart.value ?? []).some(
|
||||
(item) => item.product === props.product?.uuid
|
||||
(item) => item.productUuid === props.product?.uuid
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -186,7 +186,7 @@ const productInCartQuantity = computed(() => {
|
|||
return productEdge?.node.quantity ?? 0;
|
||||
} else {
|
||||
const cartItem = (cookieCart.value ?? []).find(
|
||||
(item) => item.product === props.product.uuid
|
||||
(item) => item.productUuid === props.product.uuid
|
||||
);
|
||||
return cartItem?.quantity ?? 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const {t} = useI18n();
|
|||
|
||||
const filteredPosts = computed(() => {
|
||||
const excludedSlugs = Object.values(docsSlugs);
|
||||
return props.posts.filter(post => !excludedSlugs.includes(post.node.slug));
|
||||
return props.posts?.filter(post => !excludedSlugs.includes(post.node.slug));
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export function useBrands(args: IBrandArgs = {}) {
|
|||
brandSearch: args.brandSearch,
|
||||
});
|
||||
|
||||
const pending = ref(false);
|
||||
const pending = ref<boolean>(false);
|
||||
const brands = ref<IBrand[]>([]);
|
||||
const pageInfo = ref<{
|
||||
hasNextPage: boolean;
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ export function useOrderOverwrite() {
|
|||
if (bulkResult?.data?.bulkOrderAction?.order) {
|
||||
cartStore.setCurrentOrders(bulkResult.data.bulkOrderAction.order);
|
||||
$notify({
|
||||
message: t('popup.success.bulkRemoveOrder'),
|
||||
message: t('popup.success.bulkAddOrder'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
|
@ -172,7 +172,7 @@ export function useOrderOverwrite() {
|
|||
const current = Array.isArray(cookieCart.value) ? [...cookieCart.value] : [];
|
||||
|
||||
const index = current.findIndex(
|
||||
(item) => item.product === args.productUuid
|
||||
(item) => item.productUuid === args.productUuid
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
|
|
@ -182,7 +182,7 @@ export function useOrderOverwrite() {
|
|||
};
|
||||
} else {
|
||||
current.push({
|
||||
product: args.productUuid,
|
||||
productUuid: args.productUuid,
|
||||
quantity: 1,
|
||||
});
|
||||
}
|
||||
|
|
@ -205,7 +205,7 @@ export function useOrderOverwrite() {
|
|||
: [];
|
||||
|
||||
const index = current.findIndex(
|
||||
(item) => item.product === args.productUuid
|
||||
(item) => item.productUuid === args.productUuid
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
|
|
@ -233,7 +233,7 @@ export function useOrderOverwrite() {
|
|||
|
||||
case 'removeKind': {
|
||||
cookieCart.value = (cookieCart.value ?? []).filter(
|
||||
(item) => item.product.uuid !== args.productUuid
|
||||
(item) => item.productUuid !== args.productUuid
|
||||
);
|
||||
|
||||
$notify({
|
||||
|
|
@ -258,18 +258,59 @@ export function useOrderOverwrite() {
|
|||
}
|
||||
|
||||
case 'bulk': {
|
||||
const bulkResult = await bulkMutate({
|
||||
orderUuid: orderUuid.value,
|
||||
action: args.bulkAction,
|
||||
products: args.products,
|
||||
const current = Array.isArray(cookieCart.value)
|
||||
? [...cookieCart.value]
|
||||
: [];
|
||||
|
||||
if (!args.products?.length) return;
|
||||
|
||||
switch (args.bulkAction) {
|
||||
case 'add': {
|
||||
args.products.forEach(({ uuid }) => {
|
||||
const index = current.findIndex(
|
||||
item => item.productUuid === uuid
|
||||
);
|
||||
|
||||
if (index !== -1) {
|
||||
current[index] = {
|
||||
...current[index],
|
||||
quantity: current[index].quantity + 1,
|
||||
};
|
||||
} else {
|
||||
current.push({
|
||||
productUuid: uuid,
|
||||
quantity: 1,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (bulkResult?.data?.bulkOrderAction?.order) {
|
||||
cartStore.setCurrentOrders(bulkResult.data.bulkOrderAction.order);
|
||||
cookieCart.value = current;
|
||||
|
||||
$notify({
|
||||
message: t('popup.success.bulkAddOrder'),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'remove': {
|
||||
const uuidsToRemove = args.products.map(p => p.uuid);
|
||||
|
||||
cookieCart.value = current.filter(
|
||||
item => !uuidsToRemove.includes(item.productUuid)
|
||||
);
|
||||
|
||||
$notify({
|
||||
message: t('popup.success.bulkRemoveOrder'),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.error('Invalid bulkAction');
|
||||
}
|
||||
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -38,13 +38,13 @@ export function useOrderSync() {
|
|||
const productsToSync = [];
|
||||
|
||||
for (const cartItem of cookieCartItems) {
|
||||
const apiQuantity = apiProductMap.get(cartItem.product.uuid) || 0;
|
||||
const apiQuantity = apiProductMap.get(cartItem.productUuid) || 0;
|
||||
const quantityDifference = cartItem.quantity - apiQuantity;
|
||||
|
||||
if (quantityDifference > 0) {
|
||||
for (let i = 0; i < quantityDifference; i++) {
|
||||
productsToSync.push({
|
||||
uuid: cartItem.product.uuid,
|
||||
uuid: cartItem.productUuid,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
37
storefront/app/composables/products/useExactProducts.ts
Normal file
37
storefront/app/composables/products/useExactProducts.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { GET_EXACT_PRODUCTS } from '@graphql/queries/standalone/products';
|
||||
import type {IProduct, IProductExactResponse} from '@types';
|
||||
|
||||
export function useExactProducts() {
|
||||
const products = ref<IProduct[]>([]);
|
||||
const error = ref<string | null>(null);
|
||||
const pending = ref<boolean>(false);
|
||||
|
||||
const getExactProducts = async (identificators: string[], identificatorType: string) => {
|
||||
pending.value = true;
|
||||
|
||||
const { data, error: mistake } = useAsyncQuery<IProductExactResponse>(GET_EXACT_PRODUCTS, {
|
||||
identificators: identificators,
|
||||
identificatorType: identificatorType
|
||||
});
|
||||
|
||||
if (data.value?.retrieveExactProducts) {
|
||||
products.value = data.value?.retrieveExactProducts;
|
||||
}
|
||||
|
||||
if (mistake.value) {
|
||||
error.value = mistake.value;
|
||||
}
|
||||
|
||||
pending.value = false;
|
||||
}
|
||||
|
||||
watch(error, (e) => {
|
||||
if (e) console.error('useExactProducts error:', e);
|
||||
});
|
||||
|
||||
return {
|
||||
products,
|
||||
pending,
|
||||
getExactProducts,
|
||||
};
|
||||
}
|
||||
|
|
@ -40,9 +40,7 @@ export function useWishlistSync() {
|
|||
type: 'bulk',
|
||||
bulkAction: 'add',
|
||||
isBulkSync: true,
|
||||
products: productsToAdd.map((p) => ({
|
||||
uuid: p,
|
||||
})),
|
||||
products: productsToAdd.map(uuid => ({ uuid }))
|
||||
});
|
||||
|
||||
if (bulkResult?.data?.bulkWishlistAction?.wishlist) {
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export const BULK_WISHLIST = gql`
|
|||
mutation bulkWishlistAction(
|
||||
$wishlistUuid: UUID!,
|
||||
$action: String!,
|
||||
$products: [BulkActionOrderProductInput]!
|
||||
$products: [BulkProductInput]!
|
||||
) {
|
||||
bulkWishlistAction(
|
||||
wishlistUuid: $wishlistUuid
|
||||
|
|
|
|||
|
|
@ -83,3 +83,18 @@ export const GET_PRODUCT_TAGS = gql`
|
|||
}
|
||||
${PRODUCT_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const GET_EXACT_PRODUCTS = gql`
|
||||
query getExactProducts(
|
||||
$identificators: [String]!,
|
||||
$identificatorType: String!
|
||||
) {
|
||||
retrieveExactProducts(
|
||||
identificators: $identificators,
|
||||
identificatorType: $identificatorType
|
||||
) {
|
||||
...Product
|
||||
}
|
||||
}
|
||||
${PRODUCT_FRAGMENT}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@
|
|||
<div class="cart__top">
|
||||
<h1 class="cart__top-title">{{ t('cart.title') }}</h1>
|
||||
<div class="cart__top-inner">
|
||||
<client-only>
|
||||
<p>({{ t('cart.items', productsInCartQuantity, { count: productsInCartQuantity }) }})</p>
|
||||
</client-only>
|
||||
<p>{{ totalPrice }}$</p>
|
||||
<ui-button
|
||||
:type="'button'"
|
||||
class="cart__top-button"
|
||||
|
|
@ -16,6 +19,7 @@
|
|||
</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
<client-only>
|
||||
<div class="cart__list">
|
||||
<div class="cart__list-inner" v-if="productsInCart.length">
|
||||
<cards-product
|
||||
|
|
@ -26,6 +30,7 @@
|
|||
</div>
|
||||
<p class="cart__empty" v-else>{{ t('cart.empty') }}</p>
|
||||
</div>
|
||||
</client-only>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -34,6 +39,7 @@
|
|||
<script setup lang="ts">
|
||||
import {usePageTitle} from "@composables/utils";
|
||||
import {useOrderBuy} from "~/composables/orders";
|
||||
import {useExactProducts} from "@composables/products/useExactProducts";
|
||||
|
||||
const {t} = useI18n();
|
||||
const cartStore = useCartStore();
|
||||
|
|
@ -48,17 +54,36 @@ const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
|
|||
});
|
||||
|
||||
const { buyOrder } = useOrderBuy();
|
||||
const { products, getExactProducts } = useExactProducts();
|
||||
|
||||
const cartUuids = computed<string[]>(() => {
|
||||
return cookieCart.value.map(item => item.productUuid);
|
||||
});
|
||||
|
||||
const productsInCart = computed(() => {
|
||||
if (isAuthenticated.value) {
|
||||
return cartStore.currentOrder ? cartStore.currentOrder.orderProducts.edges : [];
|
||||
return cartStore.currentOrder
|
||||
? cartStore.currentOrder.orderProducts.edges
|
||||
: [];
|
||||
} else {
|
||||
return cookieCart.value.map(product => ({
|
||||
return products.value.map(product => {
|
||||
const cartItem = cookieCart.value.find(
|
||||
item => item.productUuid === product.uuid
|
||||
);
|
||||
|
||||
return {
|
||||
node: {
|
||||
product: item.product,
|
||||
quantity: item.quantity
|
||||
product: product,
|
||||
quantity: cartItem?.quantity ?? 1
|
||||
}
|
||||
}));
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(async () => {
|
||||
if (!isAuthenticated.value && cartUuids.value.length) {
|
||||
await getExactProducts(cartUuids.value, 'uuid');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -66,8 +91,8 @@ const totalPrice = computed(() => {
|
|||
if (isAuthenticated.value) {
|
||||
return cartStore.currentOrder ? cartStore.currentOrder.totalPrice : 0;
|
||||
} else {
|
||||
return cookieCart.value.reduce((acc, item) => {
|
||||
return acc + (item.product.price * item.quantity);
|
||||
return productsInCart.value.reduce((acc, item) => {
|
||||
return acc + (item.node.product.price * item.node.quantity);
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -248,7 +248,7 @@ const isProductInCart = computed(() => {
|
|||
);
|
||||
} else {
|
||||
return (cookieCart.value ?? []).some(
|
||||
(item) => item.product.uuid === product.value?.uuid
|
||||
(item) => item.productUuid === product.value?.uuid
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
@ -261,7 +261,7 @@ const productInCartQuantity = computed(() => {
|
|||
return productEdge?.node.quantity ?? 0;
|
||||
} else {
|
||||
const cartItem = (cookieCart.value ?? []).find(
|
||||
(item) => item.product === product.value.uuid
|
||||
(item) => item.productUuid === product.value.uuid
|
||||
);
|
||||
return cartItem?.quantity ?? 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@
|
|||
<div class="wishlist__top">
|
||||
<h1 class="wishlist__top-title">{{ t('wishlist.title') }}</h1>
|
||||
<div class="wishlist__top-inner">
|
||||
<client-only>
|
||||
<p>({{ t('wishlist.items', productsInWishlist.length, { count: productsInWishlist.length }) }})</p>
|
||||
</client-only>
|
||||
<ui-button
|
||||
:type="'button'"
|
||||
class="wishlist__top-button"
|
||||
|
|
@ -17,6 +19,7 @@
|
|||
</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
<client-only>
|
||||
<div class="wishlist__list">
|
||||
<div class="wishlist__list-inner" v-if="productsInWishlist.length">
|
||||
<cards-product
|
||||
|
|
@ -27,6 +30,7 @@
|
|||
</div>
|
||||
<p class="wishlist__empty" v-else>{{ t('wishlist.empty') }}</p>
|
||||
</div>
|
||||
</client-only>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -36,6 +40,7 @@
|
|||
import {usePageTitle} from '@composables/utils';
|
||||
import {useWishlistOverwrite} from '@composables/wishlist';
|
||||
import {useOrderOverwrite} from "@composables/orders";
|
||||
import {useExactProducts} from "@composables/products/useExactProducts";
|
||||
|
||||
const {t} = useI18n();
|
||||
const wishlistStore = useWishlistStore();
|
||||
|
|
@ -51,24 +56,36 @@ const cookieWishlist = useCookie($appHelpers.COOKIES_WISHLIST_KEY, {
|
|||
|
||||
const { overwriteWishlist } = useWishlistOverwrite();
|
||||
const { overwriteOrder, bulkLoading } = useOrderOverwrite();
|
||||
const { products, getExactProducts } = useExactProducts();
|
||||
|
||||
const productsInWishlist = computed(() => {
|
||||
if (isAuthenticated.value) {
|
||||
return wishlistStore.wishlist ? wishlistStore.wishlist.products.edges : [];
|
||||
return wishlistStore.wishlist
|
||||
? wishlistStore.wishlist.products.edges
|
||||
: [];
|
||||
} else {
|
||||
return cookieWishlist.value.map(product => ({
|
||||
node: product
|
||||
return products.value.map(p => ({
|
||||
node: p
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(async () => {
|
||||
if (!isAuthenticated.value && cookieWishlist.value?.length) {
|
||||
await getExactProducts(cookieWishlist.value, 'uuid');
|
||||
}
|
||||
});
|
||||
|
||||
const totalPrice = computed(() => {
|
||||
return productsInWishlist.value.reduce((acc, p) => acc + p.node.price, 0);
|
||||
});
|
||||
|
||||
const selectedProducts = ref<{ uuid: string }[]>([]);
|
||||
|
||||
const productsUuid = computed<boolean>(() => {
|
||||
return productsInWishlist.value.map(p => ({ uuid: p.node.uuid }));
|
||||
const productsUuid = computed<{ uuid: string }[]>(() => {
|
||||
return productsInWishlist.value.map(p => ({
|
||||
uuid: p.node.uuid
|
||||
}));
|
||||
});
|
||||
|
||||
function toggleUuid(uuid: string, checked: boolean) {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@
|
|||
"removeAllFromWishlist": "You have successfully emptied the wishlist!",
|
||||
"bulkRemoveWishlist": "Selected items have been successfully removed from the wishlist!",
|
||||
"bulkRemoveOrder": "Selected items have been successfully removed from the cart!",
|
||||
"bulkAddOrder": "Selected items have been successfully added to the cart!",
|
||||
"avatarUpload": "You have successfully uploaded an avatar!",
|
||||
"userUpdate": "Profile successfully updated!",
|
||||
"emailUpdate": "Check your inbox for a confirmation link to complete your email update.",
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@
|
|||
"removeAllFromWishlist": "Список избранного успешно очищен!",
|
||||
"bulkRemoveWishlist": "Выбранные товары удалены из избранного!",
|
||||
"bulkRemoveOrder": "Выбранные товары удалены из корзины!",
|
||||
"bulkAddOrder": "Выбранные товары добавленны в корзину!",
|
||||
"avatarUpload": "Аватар успешно загружен!",
|
||||
"userUpdate": "Профиль успешно обновлен!",
|
||||
"emailUpdate": "Проверьте вашу почту для перехода по ссылке подтверждения и завершения обновления email.",
|
||||
|
|
@ -338,7 +339,7 @@
|
|||
"wishlist": {
|
||||
"title": "Мои Избранные",
|
||||
"items": "нет товаров | {count} товар | {count} товара | {count} товаров",
|
||||
"empty": "Список выших избранных пуст"
|
||||
"empty": "Список ваших избранных пуст"
|
||||
},
|
||||
"shop": {
|
||||
"title": "Магазин",
|
||||
|
|
|
|||
|
|
@ -32,3 +32,7 @@ export interface IBuyProductResponse {
|
|||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface IProductExactResponse {
|
||||
retrieveExactProducts: IProduct[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue