Implemented promocode application feature in the cart, allowing users to select and apply discounts during checkout. Updated GraphQL mutation, cart logic, and UI to support this functionality. - Enhanced `cart.vue` with a new promocode selection section, including dropdown and styling. - Modified `buyOrder` mutation to accept `promocodeUuid` and `forceBalance` parameters. - Updated translations (`en-gb.json` and `ru-ru.json`) to include promocode-related strings. Improves user experience by enabling discount application directly in the cart. No breaking changes.
313 lines
No EOL
7.7 KiB
Vue
313 lines
No EOL
7.7 KiB
Vue
<template>
|
|
<div class="cart">
|
|
<div class="container">
|
|
<div class="cart__wrapper">
|
|
<div class="cart__main">
|
|
<h1 class="cart__main-title">{{ t('cart.title') }}</h1>
|
|
<client-only>
|
|
<div class="cart__list">
|
|
<div class="cart__list-inner" v-if="productsInCart.length">
|
|
<cards-product
|
|
v-for="product in productsInCart"
|
|
:key="product.node.uuid"
|
|
:product="product.node.product"
|
|
/>
|
|
</div>
|
|
<p class="cart__empty" v-else>{{ t('cart.empty') }}</p>
|
|
</div>
|
|
</client-only>
|
|
</div>
|
|
<div class="cart__checkout">
|
|
<h2>{{ t('cart.checkout') }}</h2>
|
|
<client-only>
|
|
<p>{{ t('cart.items', productsInCartQuantity, { count: productsInCartQuantity }) }}</p>
|
|
</client-only>
|
|
<h6>{{ t('cart.totalPrice') }}: <span>{{ totalPrice }}$</span></h6>
|
|
<div class="cart__checkout-promo">
|
|
<button class="current" ref="button" @click="setMenuVisible(!isMenuVisible)">
|
|
<span v-if="selectedPromo">{{ selectedPromo.node.code }}</span>
|
|
<span v-else>{{ t('cart.promocode.apply') }}</span>
|
|
</button>
|
|
<transition name="fromTop">
|
|
<div class="menu" ref="menu" v-if="isMenuVisible">
|
|
<div class="menu__list" v-if="promocodes.length">
|
|
<div
|
|
class="menu__list-item"
|
|
v-for="promo of promocodes"
|
|
:key="promo.node.uuid"
|
|
@click="selectPromo(promo)"
|
|
>
|
|
<span class="code">{{ promo.node.code }}</span>
|
|
<span class="value">
|
|
<span v-if="promo.node.discountType === 'amount'">{{ promo.node.discount + '$' }}</span>
|
|
<span v-else>{{ promo.node.discount + '%' }}</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<p class="empty" v-else>{{ t('cart.promocode.empty') }}</p>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
<ui-button
|
|
:type="'button'"
|
|
class="cart__checkout-button"
|
|
@click="buyOrder(selectedPromo.node.uuid)"
|
|
>
|
|
<icon name="material-symbols:shopping-cart-checkout" size="20" />
|
|
{{ t('buttons.checkout') }}
|
|
</ui-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<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();
|
|
const userStore = useUserStore();
|
|
const promocodeStore = usePromocodeStore();
|
|
const { $appHelpers } = useNuxtApp();
|
|
|
|
const isAuthenticated = computed(() => userStore.isAuthenticated);
|
|
|
|
const promocodes = computed(() => promocodeStore.promocodes);
|
|
|
|
const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
|
|
default: () => [],
|
|
path: '/',
|
|
});
|
|
|
|
const { buyOrder } = useOrderBuy();
|
|
const { products, getExactProducts } = useExactProducts();
|
|
|
|
const menu = ref(null);
|
|
const button = ref(null);
|
|
const isMenuVisible = ref(false);
|
|
const setMenuVisible = (state) => {
|
|
isMenuVisible.value = state;
|
|
};
|
|
onClickOutside(menu, () => setMenuVisible(false), { ignore: [button] });
|
|
|
|
const selectedPromo = ref(null);
|
|
const selectPromo = (promo) => {
|
|
setMenuVisible(false);
|
|
selectedPromo.value = promo;
|
|
};
|
|
|
|
const cartUuids = computed<string[]>(() => {
|
|
return cookieCart.value.map(item => item.productUuid);
|
|
});
|
|
|
|
const productsInCart = computed(() => {
|
|
if (isAuthenticated.value) {
|
|
return cartStore.currentOrder
|
|
? cartStore.currentOrder.orderProducts.edges
|
|
: [];
|
|
} else {
|
|
return products.value.map(product => {
|
|
const cartItem = cookieCart.value.find(
|
|
item => item.productUuid === product.uuid
|
|
);
|
|
|
|
return {
|
|
node: {
|
|
product: product,
|
|
quantity: cartItem?.quantity ?? 1
|
|
}
|
|
};
|
|
});
|
|
}
|
|
});
|
|
|
|
watchEffect(async () => {
|
|
if (!isAuthenticated.value && cartUuids.value.length) {
|
|
await getExactProducts(cartUuids.value, 'uuid');
|
|
}
|
|
});
|
|
|
|
const totalPrice = computed(() => {
|
|
if (isAuthenticated.value) {
|
|
return cartStore.currentOrder ? cartStore.currentOrder.totalPrice : 0;
|
|
} else {
|
|
return productsInCart.value.reduce((acc, item) => {
|
|
return acc + (item.node.product.price * item.node.quantity);
|
|
}, 0);
|
|
}
|
|
});
|
|
|
|
const productsInCartQuantity = computed(() => {
|
|
if (isAuthenticated.value) {
|
|
let count = 0;
|
|
cartStore.currentOrder?.orderProducts?.edges.forEach((el) => {
|
|
count = count + el.node.quantity;
|
|
});
|
|
return count;
|
|
} else {
|
|
return cookieCart.value.reduce((acc, item) => acc + item.quantity, 0);
|
|
}
|
|
});
|
|
|
|
const { setPageTitle } = usePageTitle();
|
|
|
|
setPageTitle(t('breadcrumbs.cart'));
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.cart {
|
|
padding-block: 50px 100px;
|
|
background-color: $main;
|
|
|
|
&__wrapper {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 50px;
|
|
}
|
|
|
|
&__main {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 50px;
|
|
|
|
&-title {
|
|
color: $primary_dark;
|
|
font-family: "Playfair Display", sans-serif;
|
|
font-size: 36px;
|
|
font-weight: 600;
|
|
letter-spacing: -0.5px;
|
|
}
|
|
}
|
|
|
|
&__list {
|
|
&-inner {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
|
gap: 25px;
|
|
}
|
|
}
|
|
|
|
&__checkout {
|
|
width: 300px;
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 15px;
|
|
border: 1px solid $border;
|
|
border-radius: $default_border_radius;
|
|
padding: 20px;
|
|
|
|
& h2 {
|
|
margin-bottom: 20px;
|
|
color: $primary;
|
|
font-size: 24px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
& p {
|
|
color: $text;
|
|
font-size: 18px;
|
|
font-weight: 400;
|
|
letter-spacing: -0.5px;
|
|
}
|
|
|
|
& h6 {
|
|
color: $primary;
|
|
font-size: 18px;
|
|
font-weight: 500;
|
|
|
|
& span {
|
|
font-weight: 700;
|
|
}
|
|
}
|
|
|
|
&-promo {
|
|
width: 100%;
|
|
position: relative;
|
|
|
|
& .current {
|
|
width: 100%;
|
|
border-radius: $default_border_radius;
|
|
padding: 8px 12px;
|
|
font-size: 16px;
|
|
border: 1px solid $border;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
|
|
font-weight: 500;
|
|
color: $text;
|
|
}
|
|
|
|
& .menu {
|
|
position: absolute;
|
|
z-index: 2;
|
|
left: 0;
|
|
width: 100%;
|
|
top: 115%;
|
|
background-color: $main;
|
|
padding: 5px 0;
|
|
border-radius: $default_border_radius;
|
|
border: 1px solid $border;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
|
|
&__list {
|
|
width: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: start;
|
|
|
|
&-item {
|
|
cursor: pointer;
|
|
width: 100%;
|
|
padding: 6px 10px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
justify-content: space-between;
|
|
|
|
color: $text;
|
|
|
|
@include hover {
|
|
background-color: $link_secondary;
|
|
}
|
|
|
|
& .code {
|
|
font-weight: 500;
|
|
}
|
|
|
|
& .value {
|
|
font-weight: 700;
|
|
color: $primary;
|
|
}
|
|
}
|
|
}
|
|
|
|
& .empty {
|
|
padding: 4px 12px;
|
|
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: $text;
|
|
}
|
|
}
|
|
}
|
|
|
|
&-button {
|
|
margin-top: 20px;
|
|
padding-inline: 25px;
|
|
}
|
|
}
|
|
|
|
&__empty {
|
|
font-weight: 500;
|
|
font-size: 18px;
|
|
color: $primary;
|
|
}
|
|
}
|
|
</style> |