From eab2793fe1262c1e8173c3e134d644c1390561fe Mon Sep 17 00:00:00 2001 From: ninedev Date: Tue, 6 Oct 2026 11:34:29 +0300 Subject: [PATCH 1/7] Add promo code (coupon) support to order form --- common/types.ts | 16 +- components/Alert.vue | 54 +++++ components/Button.vue | 14 +- components/Input.vue | 40 +++- components/Select.vue | 17 +- components/order/Coupon.vue | 153 ++++++++++++ components/order/Customer.vue | 6 +- components/order/Fulfillment.vue | 78 ++---- components/order/Payment.vue | 26 +- components/order/Select.vue | 65 ++++- composables/useApi.ts | 14 +- nuxt.config.ts | 5 + pages/contents/order.vue | 396 ++++++++++++++++++++++++------- 13 files changed, 680 insertions(+), 204 deletions(-) create mode 100644 components/Alert.vue create mode 100644 components/order/Coupon.vue diff --git a/common/types.ts b/common/types.ts index 2bb2b8e6..2758a72b 100644 --- a/common/types.ts +++ b/common/types.ts @@ -90,16 +90,14 @@ export interface AvailableDelivery { title: string; address: string | null; mapUrl: string | null; - price: number; + price: number | null; total: number; error?: DeliveryError | false | null; // false/null - no error -} - -export interface AvailableDeliveriesInfo{ - available: AvailableDelivery[]; freeDelivery: boolean; } +export type AvailableDeliveriesInfo = AvailableDelivery[]; + export interface CustomerData{ individual:{ fio: string; @@ -140,11 +138,19 @@ export interface PaymentsInfo { default: string; } +export interface KuponNotice { + type: 'invalid_code' | 'payer_type' | 'sum_from' | 'capped' | 'outbid'; + message: string; + severity: 'error' | 'warning' | 'info'; +} + export interface OrderInfo { payerType: string; payerData: CustomerData; deliveryType: string; deliveryData: Record; + promocode: string; + promocodeNotices: KuponNotice[]; basketData: Record; countries: Record; cdekCountries: number[]; diff --git a/components/Alert.vue b/components/Alert.vue new file mode 100644 index 00000000..736a64f8 --- /dev/null +++ b/components/Alert.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/components/Button.vue b/components/Button.vue index f0e6499b..cc8b6209 100644 --- a/components/Button.vue +++ b/components/Button.vue @@ -26,6 +26,7 @@ withDefaults(defineProps<{ 'wb-button-m': size === 'default', 'wb-button-l': size === 'large', 'wb-button-primary': variant === 'primary', + 'wb-button-secondary': variant === 'secondary', 'wb-button-outlined': outlined }" :id="id"> @@ -76,9 +77,18 @@ withDefaults(defineProps<{ color: var(--primary-color) } -.wb-button-primary.wb-button-outlined { +.wb-button-secondary { + background: var(--gray-color); + border: 1px solid var(--border-color); + color: var(--heading-color); +} + +.wb-button-secondary:hover { + background: var(--border-color) !important; +} + +.wb-button-secondary.wb-button-outlined { background: transparent; - color: var(--primary-color) } .wb-button-m { diff --git a/components/Input.vue b/components/Input.vue index c00ae3ed..49e6cece 100644 --- a/components/Input.vue +++ b/components/Input.vue @@ -14,6 +14,7 @@ const props = defineProps<{ pattern?: string; title?: string; errorMessage?: string; + size?: 'default' | 'large'; }>(); const model = defineModel(); @@ -49,7 +50,9 @@ const hasError = computed(() => error.value || !!props.errorMessage); class="input" :class="{ 'input-filled': !!model, - 'input-error': hasError + 'input-error': hasError, + 'input-hasAction': !!$slots.action, + 'input-large': size === 'large' }" :id="id" :autofocus="autofocus" @@ -62,12 +65,15 @@ const hasError = computed(() => error.value || !!props.errorMessage); @blur="validate" @input="validate" /> -
{{ model }}
+
{{ model }}
+
+ +
{{ errorMessage }} @@ -82,7 +88,7 @@ const hasError = computed(() => error.value || !!props.errorMessage); color: #000; font-family: 'Plumb', sans-serif; border: 1px solid var(--border-color); - padding: 26px 12px 18px; + padding: 26px 12px 8px; transition: background 0.2s, border 0.2s, outline-offset 0.2s; font-size: 24px; line-height: 24px; @@ -106,17 +112,19 @@ const hasError = computed(() => error.value || !!props.errorMessage); .input-label { position: absolute; z-index: 1; - top: 26px; + top: 50%; + transform: translateY(-50%); left: 12px; color: #ccc; font-size: 16px; font-weight: 500; - transition: top 0.2s, font-weight 0.2s, font-size 0.2s; + transition: top 0.2s, font-weight 0.2s, font-size 0.2s, transform 0.2s; } .input:not(:placeholder-shown) + .input-label, .input:focus + .input-label { top: 8px; + transform: translateY(0); font-size: 14px; } @@ -134,8 +142,16 @@ const hasError = computed(() => error.value || !!props.errorMessage); color: #aaa; } -.input-static { +.input-large { + padding-bottom: 18px; +} + +.input-static.input-large { min-height: 74px; +} + +.input-static { + min-height: 64px; height: auto; white-space: normal; overflow-wrap: anywhere; @@ -144,6 +160,18 @@ const hasError = computed(() => error.value || !!props.errorMessage); cursor: not-allowed; } +.input-hasAction { + padding-right: 112px; +} + +.input-action { + position: absolute; + z-index: 1; + right: 8px; + top: 50%; + transform: translateY(-50%); +} + .input-errorMessage { color: var(--danger-color); font-size: 13px; diff --git a/components/Select.vue b/components/Select.vue index 31b67791..1e4d49f8 100644 --- a/components/Select.vue +++ b/components/Select.vue @@ -8,7 +8,7 @@ defineProps<{ placeholder: string; disabled?: boolean; showClear?: boolean; - size?: 'small' | 'default'; + size?: 'small' | 'default' | 'large'; isSearchable: boolean; changeCallback?: () => void; }>(); @@ -20,7 +20,8 @@ const model = defineModel();