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 {
|
.el-select__wrapper {
|
||||||
height: 36px !important;
|
height: 36px !important;
|
||||||
min-height: 50px !important;
|
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
}
|
}
|
||||||
.el-select--large .el-select__wrapper {
|
.el-select--large .el-select__wrapper {
|
||||||
|
|
@ -25,3 +24,11 @@
|
||||||
color: $primary !important;
|
color: $primary !important;
|
||||||
background-color: $main !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>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form__block">
|
<div class="form__block">
|
||||||
<p>{{ t('fields.address') }}</p>
|
<p>{{ t('fields.addressAuto') }}</p>
|
||||||
<el-select
|
<el-select
|
||||||
size="large"
|
size="large"
|
||||||
filterable
|
filterable
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import { BUY_CART } from '@graphql/mutations/cart';
|
import { BUY_CART } from '@graphql/mutations/cart';
|
||||||
import type { IBuyOrderResponse } from '@types';
|
import type { IBuyOrderResponse } from '@types';
|
||||||
|
|
||||||
|
interface IBuyOrderArguments {
|
||||||
|
promocodeUuid: string,
|
||||||
|
billingAddress: string,
|
||||||
|
shippingAddress: string
|
||||||
|
}
|
||||||
|
|
||||||
export function useOrderBuy() {
|
export function useOrderBuy() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { $notify } = useNuxtApp();
|
const { $notify } = useNuxtApp();
|
||||||
|
|
@ -10,12 +16,14 @@ export function useOrderBuy() {
|
||||||
|
|
||||||
const { mutate, loading, error } = useMutation<IBuyOrderResponse>(BUY_CART);
|
const { mutate, loading, error } = useMutation<IBuyOrderResponse>(BUY_CART);
|
||||||
|
|
||||||
async function buyOrder(promocodeUuid?: string) {
|
async function buyOrder(args: IBuyOrderArguments) {
|
||||||
const result = await mutate({
|
const result = await mutate({
|
||||||
orderUuid: orderUuid.value,
|
orderUuid: orderUuid.value,
|
||||||
forcePayment: true,
|
forcePayment: true,
|
||||||
forceBalance: false,
|
forceBalance: false,
|
||||||
promocodeUuid: promocodeUuid
|
promocodeUuid: args.promocodeUuid,
|
||||||
|
billingAddress: args.billingAddress,
|
||||||
|
shippingAddress: args.shippingAddress,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result?.data?.buyOrder?.transaction?.process?.url) {
|
if (result?.data?.buyOrder?.transaction?.process?.url) {
|
||||||
|
|
|
||||||
|
|
@ -23,35 +23,83 @@
|
||||||
<p>{{ t('cart.items', productsInCartQuantity, { count: productsInCartQuantity }) }}</p>
|
<p>{{ t('cart.items', productsInCartQuantity, { count: productsInCartQuantity }) }}</p>
|
||||||
</client-only>
|
</client-only>
|
||||||
<h6>{{ t('cart.totalPrice') }}: <span>{{ totalPrice }}$</span></h6>
|
<h6>{{ t('cart.totalPrice') }}: <span>{{ totalPrice }}$</span></h6>
|
||||||
<div class="cart__checkout-promo">
|
<!-- <div class="cart__checkout-promo">-->
|
||||||
<button class="current" ref="button" @click="setMenuVisible(!isMenuVisible)">
|
<!-- <button class="current" ref="button" @click="setMenuVisible(!isMenuVisible)">-->
|
||||||
<span v-if="selectedPromo">{{ selectedPromo.node.code }}</span>
|
<!-- <span v-if="selectedPromo">{{ selectedPromo.node.code }}</span>-->
|
||||||
<span v-else>{{ t('cart.promocode.apply') }}</span>
|
<!-- <span v-else>{{ t('cart.promocode.apply') }}</span>-->
|
||||||
</button>
|
<!-- </button>-->
|
||||||
<transition name="fromTop">
|
<!-- <transition name="fromTop">-->
|
||||||
<div class="menu" ref="menu" v-if="isMenuVisible">
|
<!-- <div class="menu" ref="menu" v-if="isMenuVisible">-->
|
||||||
<div class="menu__list" v-if="promocodes.length">
|
<!-- <div class="menu__list" v-if="promocodes.length">-->
|
||||||
<div
|
<!-- <div-->
|
||||||
class="menu__list-item"
|
<!-- class="menu__list-item"-->
|
||||||
v-for="promo of promocodes"
|
<!-- v-for="promo of promocodes"-->
|
||||||
:key="promo.node.uuid"
|
<!-- :key="promo.node.uuid"-->
|
||||||
@click="selectPromo(promo)"
|
<!-- @click="selectPromo(promo)"-->
|
||||||
>
|
<!-- >-->
|
||||||
<span class="code">{{ promo.node.code }}</span>
|
<!-- <span class="code">{{ promo.node.code }}</span>-->
|
||||||
<span class="value">
|
<!-- <span class="value">-->
|
||||||
<span v-if="promo.node.discountType === 'amount'">{{ promo.node.discount + '$' }}</span>
|
<!-- <span v-if="promo.node.discountType === 'amount'">{{ promo.node.discount + '$' }}</span>-->
|
||||||
<span v-else>{{ promo.node.discount + '%' }}</span>
|
<!-- <span v-else>{{ promo.node.discount + '%' }}</span>-->
|
||||||
</span>
|
<!-- </span>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
<p class="empty" v-else>{{ t('cart.promocode.empty') }}</p>
|
<!-- <p class="empty" v-else>{{ t('cart.promocode.empty') }}</p>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
</transition>
|
<!-- </transition>-->
|
||||||
</div>
|
<!-- </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
|
<ui-button
|
||||||
:type="'button'"
|
:type="'button'"
|
||||||
class="cart__checkout-button"
|
class="cart__checkout-button"
|
||||||
@click="buyOrder(selectedPromo.node.uuid)"
|
:isDisabled="!isFormValid"
|
||||||
|
@click="handleBuy"
|
||||||
>
|
>
|
||||||
<icon name="material-symbols:shopping-cart-checkout" size="20" />
|
<icon name="material-symbols:shopping-cart-checkout" size="20" />
|
||||||
{{ t('buttons.checkout') }}
|
{{ t('buttons.checkout') }}
|
||||||
|
|
@ -66,15 +114,18 @@
|
||||||
import {usePageTitle} from "@composables/utils";
|
import {usePageTitle} from "@composables/utils";
|
||||||
import {useOrderBuy} from "~/composables/orders";
|
import {useOrderBuy} from "~/composables/orders";
|
||||||
import {useExactProducts} from "@composables/products/useExactProducts";
|
import {useExactProducts} from "@composables/products/useExactProducts";
|
||||||
|
import {useValidators} from "@composables/rules";
|
||||||
|
|
||||||
const {t} = useI18n();
|
const {t} = useI18n();
|
||||||
const cartStore = useCartStore();
|
const cartStore = useCartStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const promocodeStore = usePromocodeStore();
|
const promocodeStore = usePromocodeStore();
|
||||||
|
const addressesStore = useAddressesStore();
|
||||||
const { $appHelpers } = useNuxtApp();
|
const { $appHelpers } = useNuxtApp();
|
||||||
|
|
||||||
const isAuthenticated = computed(() => userStore.isAuthenticated);
|
const isAuthenticated = computed(() => userStore.isAuthenticated);
|
||||||
|
|
||||||
|
const addresses = computed(() => addressesStore.addresses);
|
||||||
const promocodes = computed(() => promocodeStore.promocodes);
|
const promocodes = computed(() => promocodeStore.promocodes);
|
||||||
|
|
||||||
const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
|
const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
|
||||||
|
|
@ -82,6 +133,7 @@ const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, {
|
||||||
path: '/',
|
path: '/',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { required, isEmail } = useValidators();
|
||||||
const { buyOrder } = useOrderBuy();
|
const { buyOrder } = useOrderBuy();
|
||||||
const { products, getExactProducts } = useExactProducts();
|
const { products, getExactProducts } = useExactProducts();
|
||||||
|
|
||||||
|
|
@ -99,6 +151,10 @@ const selectPromo = (promo) => {
|
||||||
selectedPromo.value = promo;
|
selectedPromo.value = promo;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const promo = ref<string>('');
|
||||||
|
const billingAddress = ref<string>('');
|
||||||
|
const shippingAddress = ref<string>('');
|
||||||
|
|
||||||
const cartUuids = computed<string[]>(() => {
|
const cartUuids = computed<string[]>(() => {
|
||||||
return cookieCart.value.map(item => item.productUuid);
|
return cookieCart.value.map(item => item.productUuid);
|
||||||
});
|
});
|
||||||
|
|
@ -154,6 +210,21 @@ const productsInCartQuantity = computed(() => {
|
||||||
|
|
||||||
const { setPageTitle } = usePageTitle();
|
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'));
|
setPageTitle(t('breadcrumbs.cart'));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,11 @@
|
||||||
"confirmPassword": "Confirm password",
|
"confirmPassword": "Confirm password",
|
||||||
"confirmNewPassword": "Confirm new password",
|
"confirmNewPassword": "Confirm new password",
|
||||||
"brandsSearch": "Search brands by name...",
|
"brandsSearch": "Search brands by name...",
|
||||||
"promocode": "Enter promocode",
|
"promocode": "Promocode",
|
||||||
"address": "Start typing the address",
|
"billingAddress": "Billing address",
|
||||||
|
"shippingAddress": "Shipping Address",
|
||||||
|
"address": "Address",
|
||||||
|
"addressAuto": "Start typing the address",
|
||||||
"addressName": "Name",
|
"addressName": "Name",
|
||||||
"country": "Country",
|
"country": "Country",
|
||||||
"region": "Region",
|
"region": "Region",
|
||||||
|
|
|
||||||
|
|
@ -55,8 +55,11 @@
|
||||||
"confirmPassword": "Подтвердите пароль",
|
"confirmPassword": "Подтвердите пароль",
|
||||||
"confirmNewPassword": "Подтвердите новый пароль",
|
"confirmNewPassword": "Подтвердите новый пароль",
|
||||||
"brandsSearch": "Поиск брендов по названию...",
|
"brandsSearch": "Поиск брендов по названию...",
|
||||||
"promocode": "Введите промокод",
|
"promocode": "Промокод",
|
||||||
"address": "Начните вводить адрес",
|
"billingAddress": "Адрес для выставления счетов",
|
||||||
|
"shippingAddress": "Адрес доставки",
|
||||||
|
"address": "Адрес",
|
||||||
|
"addressAuto": "Начните вводить адрес",
|
||||||
"addressName": "Название",
|
"addressName": "Название",
|
||||||
"country": "Страна",
|
"country": "Страна",
|
||||||
"region": "Регион",
|
"region": "Регион",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue