From 69c224722e87e9eaaa8b92b5d3bc6b5363c18ff1 Mon Sep 17 00:00:00 2001 From: Alexandr SaVBaD Waltz Date: Tue, 3 Mar 2026 15:23:11 +0300 Subject: [PATCH] feat(storefront): add promocode support in cart with UI and logic updates 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. --- .../app/components/base/footer/index.vue | 2 +- .../app/composables/orders/useOrderBuy.ts | 4 +- storefront/app/graphql/mutations/cart.ts | 6 +- storefront/app/pages/cart.vue | 237 ++++++++++++++---- storefront/i18n/locales/en-gb.json | 11 +- storefront/i18n/locales/ru-ru.json | 11 +- 6 files changed, 215 insertions(+), 56 deletions(-) diff --git a/storefront/app/components/base/footer/index.vue b/storefront/app/components/base/footer/index.vue index ba67b4c6..b1466398 100644 --- a/storefront/app/components/base/footer/index.vue +++ b/storefront/app/components/base/footer/index.vue @@ -103,7 +103,7 @@ const encodedCompanyAddress = computed(() => { gap: 14px; & h6 { - color: $link_primary; + color: #d2d0d0; font-size: 16px; font-weight: 600; letter-spacing: -0.5px; diff --git a/storefront/app/composables/orders/useOrderBuy.ts b/storefront/app/composables/orders/useOrderBuy.ts index 26eab131..5812a129 100644 --- a/storefront/app/composables/orders/useOrderBuy.ts +++ b/storefront/app/composables/orders/useOrderBuy.ts @@ -10,10 +10,12 @@ export function useOrderBuy() { const { mutate, loading, error } = useMutation(BUY_CART); - async function buyOrder() { + async function buyOrder(promocodeUuid?: string) { const result = await mutate({ orderUuid: orderUuid.value, forcePayment: true, + forceBalance: false, + promocodeUuid: promocodeUuid }); if (result?.data?.buyOrder?.transaction?.process?.url) { diff --git a/storefront/app/graphql/mutations/cart.ts b/storefront/app/graphql/mutations/cart.ts index 913fd656..99eb19e4 100644 --- a/storefront/app/graphql/mutations/cart.ts +++ b/storefront/app/graphql/mutations/cart.ts @@ -88,11 +88,15 @@ export const BULK_CART = gql` export const BUY_CART = gql` mutation buyOrder( $orderUuid: String!, - $forcePayment: Boolean! + $forcePayment: Boolean, + $forceBalance: Boolean, + $promocodeUuid: String, ) { buyOrder( orderUuid: $orderUuid forcePayment: $forcePayment + forceBalance: $forceBalance + promocodeUuid: $promocodeUuid ) { transaction { amount diff --git a/storefront/app/pages/cart.vue b/storefront/app/pages/cart.vue index 267da952..14112de3 100644 --- a/storefront/app/pages/cart.vue +++ b/storefront/app/pages/cart.vue @@ -2,35 +2,61 @@
-
-

{{ t('cart.title') }}

-
- -

({{ t('cart.items', productsInCartQuantity, { count: productsInCartQuantity }) }})

-
-

{{ totalPrice }}$

- - - {{ t('buttons.checkout') }} - -
-
- -
-
- +
+

{{ t('cart.title') }}

+ +
+
+ +
+

{{ t('cart.empty') }}

-

{{ t('cart.empty') }}

+
+
+
+

{{ t('cart.checkout') }}

+ +

{{ t('cart.items', productsInCartQuantity, { count: productsInCartQuantity }) }}

+
+
{{ t('cart.totalPrice') }}: {{ totalPrice }}$
+
+ + + +
- + + + {{ t('buttons.checkout') }} + +
@@ -44,10 +70,13 @@ 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: '/', @@ -56,6 +85,20 @@ const cookieCart = useCookie($appHelpers.COOKIES_CART_KEY, { 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(() => { return cookieCart.value.map(item => item.productUuid); }); @@ -121,20 +164,15 @@ setPageTitle(t('breadcrumbs.cart')); &__wrapper { display: flex; - flex-direction: column; + align-items: flex-start; gap: 50px; } - &__top { + &__main { + width: 100%; display: flex; - align-items: center; - justify-content: space-between; - - &-inner { - display: flex; - align-items: center; - gap: 35px; - } + flex-direction: column; + gap: 50px; &-title { color: $primary_dark; @@ -143,18 +181,6 @@ setPageTitle(t('breadcrumbs.cart')); font-weight: 600; letter-spacing: -0.5px; } - - & p { - color: $text; - font-size: 18px; - font-weight: 400; - letter-spacing: -0.5px; - } - - &-button { - width: fit-content; - padding-inline: 25px; - } } &__list { @@ -165,6 +191,119 @@ setPageTitle(t('breadcrumbs.cart')); } } + &__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; diff --git a/storefront/i18n/locales/en-gb.json b/storefront/i18n/locales/en-gb.json index fc44c8a5..56f3d526 100644 --- a/storefront/i18n/locales/en-gb.json +++ b/storefront/i18n/locales/en-gb.json @@ -48,7 +48,8 @@ "newPassword": "New password", "confirmPassword": "Confirm password", "confirmNewPassword": "Confirm new password", - "brandsSearch": "Search brands by name..." + "brandsSearch": "Search brands by name...", + "promocode": "Enter promocode" }, "checkboxes": { "remember": "Remember me", @@ -332,9 +333,15 @@ "text": "Discover the latest trends, style inspiration, and fashion insights from our editorial team." }, "cart": { + "totalPrice": "Total price", "title": "My Cart", "items": "no items | {count} item | {count} items", - "empty": "Your cart is empty." + "empty": "Your cart is empty.", + "checkout": "Checkout", + "promocode": { + "apply": "Apply a promocode", + "empty": "You don't have any promocodes" + } }, "wishlist": { "title": "My Wishlist", diff --git a/storefront/i18n/locales/ru-ru.json b/storefront/i18n/locales/ru-ru.json index fb95c107..75b56de5 100644 --- a/storefront/i18n/locales/ru-ru.json +++ b/storefront/i18n/locales/ru-ru.json @@ -48,7 +48,8 @@ "newPassword": "Новый пароль", "confirmPassword": "Подтвердите пароль", "confirmNewPassword": "Подтвердите новый пароль", - "brandsSearch": "Поиск брендов по названию..." + "brandsSearch": "Поиск брендов по названию...", + "promocode": "Введите промокод" }, "checkboxes": { "remember": "Запомнить меня", @@ -332,9 +333,15 @@ "text": "Узнавайте о последних трендах, источниках вдохновения для стиля и модных инсайтах от нашей редакции." }, "cart": { + "totalPrice": "Итог", "title": "Моя корзина", "items": "нет товаров | {count} товар | {count} товара | {count} товаров", - "empty": "Ваша корзина пуста" + "empty": "Ваша корзина пуста", + "checkout": "Оформление", + "promocode": { + "apply": "Примените промокод", + "empty": "У вас нет промокодов" + } }, "wishlist": { "title": "Мои Избранные",