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:
parent
048da0e251
commit
52e559dae0
6 changed files with 126 additions and 34 deletions
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
.el-select__wrapper {
|
||||
height: 36px !important;
|
||||
min-height: 50px !important;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
.el-select--large .el-select__wrapper {
|
||||
|
|
@ -24,4 +23,12 @@
|
|||
.el-select-dropdown__item.is-hovering {
|
||||
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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -55,8 +55,11 @@
|
|||
"confirmPassword": "Подтвердите пароль",
|
||||
"confirmNewPassword": "Подтвердите новый пароль",
|
||||
"brandsSearch": "Поиск брендов по названию...",
|
||||
"promocode": "Введите промокод",
|
||||
"address": "Начните вводить адрес",
|
||||
"promocode": "Промокод",
|
||||
"billingAddress": "Адрес для выставления счетов",
|
||||
"shippingAddress": "Адрес доставки",
|
||||
"address": "Адрес",
|
||||
"addressAuto": "Начните вводить адрес",
|
||||
"addressName": "Название",
|
||||
"country": "Страна",
|
||||
"region": "Регион",
|
||||
|
|
|
|||
Loading…
Reference in a new issue