feat(cart): add address and promocode selection to checkout

Implemented billing and shipping address selection in the cart UI, along with improved promocode dropdown. Updated GraphQL mutation to accept new address fields for more comprehensive order handling.

- Replaced the old promocode selection implementation with `el-select` components.
- Introduced billing and shipping address fields with selectable options.
- Enhanced form validation to ensure all required fields are populated before checkout.
- Updated translations (`en-gb.json`, `ru-ru.json`) with new field labels.
- Adjusted SCSS for consistent styling of dropdowns.

Improves user experience by streamlining and enhancing the checkout process. No breaking changes introduced.
This commit is contained in:
Alexandr SaVBaD Waltz 2026-03-10 11:44:09 +03:00
parent 048da0e251
commit 52e559dae0
6 changed files with 126 additions and 34 deletions

View file

@ -2,7 +2,6 @@
.el-select__wrapper {
height: 36px !important;
min-height: 50px !important;
background-color: transparent !important;
}
.el-select--large .el-select__wrapper {
@ -25,3 +24,11 @@
color: $primary !important;
background-color: $main !important;
}
.el-select--small .el-select__wrapper {
height: 30px !important;
}
.el-select--small .el-select-dropdown__item {
height: 28px !important;
}

View file

@ -16,7 +16,7 @@
</el-select>
</div>
<div class="form__block">
<p>{{ t('fields.address') }}</p>
<p>{{ t('fields.addressAuto') }}</p>
<el-select
size="large"
filterable

View file

@ -1,6 +1,12 @@
import { BUY_CART } from '@graphql/mutations/cart';
import type { IBuyOrderResponse } from '@types';
interface IBuyOrderArguments {
promocodeUuid: string,
billingAddress: string,
shippingAddress: string
}
export function useOrderBuy() {
const { t } = useI18n();
const { $notify } = useNuxtApp();
@ -10,12 +16,14 @@ export function useOrderBuy() {
const { mutate, loading, error } = useMutation<IBuyOrderResponse>(BUY_CART);
async function buyOrder(promocodeUuid?: string) {
async function buyOrder(args: IBuyOrderArguments) {
const result = await mutate({
orderUuid: orderUuid.value,
forcePayment: true,
forceBalance: false,
promocodeUuid: promocodeUuid
promocodeUuid: args.promocodeUuid,
billingAddress: args.billingAddress,
shippingAddress: args.shippingAddress,
});
if (result?.data?.buyOrder?.transaction?.process?.url) {

View file

@ -23,35 +23,83 @@
<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>
<!-- <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>-->
<el-select
v-model="billingAddress"
:placeholder="t('fields.billingAddress')"
size="small"
>
<el-option
v-for="item in addresses"
:key="item.node.uuid"
:label="item.node.addressLine.split(' ').find(el => el.includes('name'))?.split('=')[1]?.split('_').join(' ')"
:value="item.node.uuid"
/>
</el-select>
<el-select
v-model="shippingAddress"
:placeholder="t('fields.shippingAddress')"
size="small"
>
<el-option
v-for="item in addresses"
:key="item.node.uuid"
:label="item.node.addressLine.split(' ').find(el => el.includes('name'))?.split('=')[1]?.split('_').join(' ')"
:value="item.node.uuid"
/>
</el-select>
<el-select
v-model="promo"
:placeholder="t('fields.promocode')"
size="small"
>
<el-option
v-for="item in promocodes"
:key="item.node.uuid"
:label="item.node.code"
:value="item.node.uuid"
>
<span>
{{ item.node.code }}
<b>
{{
item.node.discountType === 'amount'
? item.node.discount + '$'
: item.node.discount + '%'
}}
</b>
</span>
</el-option>
</el-select>
<ui-button
:type="'button'"
class="cart__checkout-button"
@click="buyOrder(selectedPromo.node.uuid)"
:isDisabled="!isFormValid"
@click="handleBuy"
>
<icon name="material-symbols:shopping-cart-checkout" size="20" />
{{ t('buttons.checkout') }}
@ -66,15 +114,18 @@
import {usePageTitle} from "@composables/utils";
import {useOrderBuy} from "~/composables/orders";
import {useExactProducts} from "@composables/products/useExactProducts";
import {useValidators} from "@composables/rules";
const {t} = useI18n();
const cartStore = useCartStore();
const userStore = useUserStore();
const promocodeStore = usePromocodeStore();
const addressesStore = useAddressesStore();
const { $appHelpers } = useNuxtApp();
const isAuthenticated = computed(() => userStore.isAuthenticated);
const addresses = computed(() => addressesStore.addresses);
const promocodes = computed(() => promocodeStore.promocodes);
const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
@ -82,6 +133,7 @@ const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
path: '/',
});
const { required, isEmail } = useValidators();
const { buyOrder } = useOrderBuy();
const { products, getExactProducts } = useExactProducts();
@ -99,6 +151,10 @@ const selectPromo = (promo) => {
selectedPromo.value = promo;
};
const promo = ref<string>('');
const billingAddress = ref<string>('');
const shippingAddress = ref<string>('');
const cartUuids = computed<string[]>(() => {
return cookieCart.value.map(item => item.productUuid);
});
@ -154,6 +210,21 @@ const productsInCartQuantity = computed(() => {
const { setPageTitle } = usePageTitle();
const isFormValid = computed(() => {
return (
required(billingAddress.value) === true &&
required(shippingAddress.value) === true
);
});
const handleBuy = async () => {
await buyOrder({
promocodeUuid: promo.value,
billingAddress: billingAddress.value,
shippingAddress: shippingAddress.value
})
}
setPageTitle(t('breadcrumbs.cart'));
</script>

View file

@ -55,8 +55,11 @@
"confirmPassword": "Confirm password",
"confirmNewPassword": "Confirm new password",
"brandsSearch": "Search brands by name...",
"promocode": "Enter promocode",
"address": "Start typing the address",
"promocode": "Promocode",
"billingAddress": "Billing address",
"shippingAddress": "Shipping Address",
"address": "Address",
"addressAuto": "Start typing the address",
"addressName": "Name",
"country": "Country",
"region": "Region",

View file

@ -55,8 +55,11 @@
"confirmPassword": "Подтвердите пароль",
"confirmNewPassword": "Подтвердите новый пароль",
"brandsSearch": "Поиск брендов по названию...",
"promocode": "Введите промокод",
"address": "Начните вводить адрес",
"promocode": "Промокод",
"billingAddress": "Адрес для выставления счетов",
"shippingAddress": "Адрес доставки",
"address": "Адрес",
"addressAuto": "Начните вводить адрес",
"addressName": "Название",
"country": "Страна",
"region": "Регион",