@mundogamernetwork/shared-ui 1.1.3 → 1.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/checkout/MgCartItemList.vue +94 -43
- package/components/checkout/MgCartSummary.vue +100 -137
- package/components/checkout/MgCouponInput.vue +211 -0
- package/components/ui/MgConfirmationPage.vue +415 -0
- package/components/ui/MgLoginRegisterMenu.vue +1 -1
- package/components/ui/MgPaymentMethods.vue +134 -0
- package/composables/useConfirmation.ts +34 -5
- package/composables/useMgCheckout.ts +398 -166
- package/package.json +3 -2
- package/public/sounds/notification.mp3 +0 -0
- package/utils/authRedirect.ts +2 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { computed } from "vue";
|
|
2
3
|
import type { CartItem } from "../../composables/useMgCheckout";
|
|
4
|
+
import { injectMgCheckout } from "../../composables/useMgCheckout";
|
|
3
5
|
|
|
4
|
-
defineProps<{
|
|
5
|
-
items
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
items?: CartItem[];
|
|
6
8
|
loading?: boolean;
|
|
7
9
|
editable?: boolean;
|
|
8
10
|
currencySymbol?: string;
|
|
11
|
+
/** Auto-wire to parent useMgCheckout context */
|
|
12
|
+
autoWire?: boolean;
|
|
9
13
|
}>();
|
|
10
14
|
|
|
11
15
|
const emit = defineEmits<{
|
|
@@ -13,6 +17,49 @@ const emit = defineEmits<{
|
|
|
13
17
|
"remove-item": [id: number, type: string];
|
|
14
18
|
"clear-all": [];
|
|
15
19
|
}>();
|
|
20
|
+
|
|
21
|
+
const checkout = injectMgCheckout();
|
|
22
|
+
|
|
23
|
+
const resolvedItems = computed<CartItem[]>(() =>
|
|
24
|
+
props.autoWire ? (checkout?.orderCart.value ?? []) : (props.items ?? [])
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const isLoading = computed(() =>
|
|
28
|
+
props.autoWire ? checkout?.cartLoading.value : props.loading
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const sym = computed(() => props.currencySymbol ?? "$");
|
|
32
|
+
|
|
33
|
+
/** Parse price safely — handles both numbers and strings like "49,90" */
|
|
34
|
+
function parsePrice(v: string | number | null | undefined): number {
|
|
35
|
+
if (v == null) return 0;
|
|
36
|
+
if (typeof v === "number") return v;
|
|
37
|
+
return parseFloat(String(v).replace(",", ".").replace(/[^0-9.]/g, "")) || 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function fmtLine(price: string | number | null | undefined, qty: number): string {
|
|
41
|
+
const p = parsePrice(price);
|
|
42
|
+
if (typeof price === "string" && /[^\d.,]/.test(price)) {
|
|
43
|
+
// Already contains currency symbol — just show as-is (single unit)
|
|
44
|
+
return price;
|
|
45
|
+
}
|
|
46
|
+
return `${sym.value}${(p * qty).toFixed(2)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function handleAdd(id: number, type: string) {
|
|
50
|
+
if (props.autoWire && checkout) await checkout.addItem(id, type);
|
|
51
|
+
else emit("add-item", id, type);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function handleRemove(id: number, type: string) {
|
|
55
|
+
if (props.autoWire && checkout) await checkout.removeItem(id, type);
|
|
56
|
+
else emit("remove-item", id, type);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function handleClear() {
|
|
60
|
+
if (props.autoWire && checkout) await checkout.clearCartItems();
|
|
61
|
+
else emit("clear-all");
|
|
62
|
+
}
|
|
16
63
|
</script>
|
|
17
64
|
|
|
18
65
|
<template>
|
|
@@ -23,20 +70,23 @@ const emit = defineEmits<{
|
|
|
23
70
|
<span class="col-value">{{ $t?.("checkout.cart.value") ?? "Value" }}</span>
|
|
24
71
|
</div>
|
|
25
72
|
|
|
26
|
-
|
|
73
|
+
<!-- Loading skeleton -->
|
|
74
|
+
<div v-if="isLoading" class="mg-cart-items__loading">
|
|
27
75
|
<div v-for="i in 2" :key="i" class="skeleton-row">
|
|
28
|
-
<div class="skeleton-block" style="width: 60%; height: 14px"
|
|
29
|
-
<div class="skeleton-block" style="width: 20%; height: 14px"
|
|
76
|
+
<div class="skeleton-block" style="width: 60%; height: 14px;" />
|
|
77
|
+
<div class="skeleton-block" style="width: 20%; height: 14px;" />
|
|
30
78
|
</div>
|
|
31
79
|
</div>
|
|
32
80
|
|
|
33
|
-
|
|
81
|
+
<!-- Empty -->
|
|
82
|
+
<div v-else-if="resolvedItems.length === 0" class="mg-cart-items__empty">
|
|
34
83
|
<p>{{ $t?.("checkout.cart.empty") ?? "Your cart is empty" }}</p>
|
|
35
84
|
</div>
|
|
36
85
|
|
|
86
|
+
<!-- Items -->
|
|
37
87
|
<template v-else>
|
|
38
88
|
<div
|
|
39
|
-
v-for="item in
|
|
89
|
+
v-for="item in resolvedItems"
|
|
40
90
|
:key="`${item.item_type}-${item.item_id}`"
|
|
41
91
|
class="mg-cart-item"
|
|
42
92
|
>
|
|
@@ -44,35 +94,43 @@ const emit = defineEmits<{
|
|
|
44
94
|
<img
|
|
45
95
|
v-if="item.item?.image_url"
|
|
46
96
|
:src="item.item.image_url"
|
|
47
|
-
:alt="item.item?.name"
|
|
97
|
+
:alt="item.item?.localized_name ?? item.item?.name"
|
|
48
98
|
class="mg-cart-item__img"
|
|
49
99
|
/>
|
|
50
|
-
<span class="mg-cart-item__name">
|
|
100
|
+
<span class="mg-cart-item__name">
|
|
101
|
+
{{ item.item?.localized_name ?? item.item?.name ?? $t?.("checkout.cart.item") ?? "Product" }}
|
|
102
|
+
</span>
|
|
51
103
|
</div>
|
|
52
104
|
|
|
53
105
|
<div class="col-qty">
|
|
54
106
|
<template v-if="editable">
|
|
55
|
-
<button class="qty-btn" @click="
|
|
107
|
+
<button class="qty-btn" :disabled="isLoading" @click="handleRemove(item.item_id, item.item_type)">−</button>
|
|
56
108
|
<span class="qty-value">{{ item.quantity }}</span>
|
|
57
|
-
<button class="qty-btn" @click="
|
|
109
|
+
<button class="qty-btn" :disabled="isLoading" @click="handleAdd(item.item_id, item.item_type)">+</button>
|
|
58
110
|
</template>
|
|
59
111
|
<span v-else class="qty-value">{{ item.quantity }}</span>
|
|
60
112
|
</div>
|
|
61
113
|
|
|
62
114
|
<div class="col-value">
|
|
63
|
-
|
|
64
|
-
|
|
115
|
+
<!-- Show crossed-out original if there's a discount -->
|
|
116
|
+
<span
|
|
117
|
+
v-if="item.discount && parsePrice(item.discount) > 0"
|
|
118
|
+
class="original-price"
|
|
119
|
+
>
|
|
120
|
+
{{ fmtLine(item.price, item.quantity) }}
|
|
65
121
|
</span>
|
|
66
122
|
<span class="final-price">
|
|
67
|
-
{{
|
|
123
|
+
{{ fmtLine(item.price_with_discount ?? item.price, item.quantity) }}
|
|
68
124
|
</span>
|
|
69
125
|
</div>
|
|
70
126
|
</div>
|
|
71
127
|
|
|
128
|
+
<!-- Clear all button -->
|
|
72
129
|
<div v-if="editable" class="mg-cart-items__footer">
|
|
73
|
-
<button class="clear-btn" @click="
|
|
130
|
+
<button class="clear-btn" :disabled="isLoading" @click="handleClear">
|
|
74
131
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
75
|
-
<polyline points="3 6 5 6 21 6"
|
|
132
|
+
<polyline points="3 6 5 6 21 6" />
|
|
133
|
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
76
134
|
</svg>
|
|
77
135
|
{{ $t?.("checkout.cart.clear") ?? "Clear all" }}
|
|
78
136
|
</button>
|
|
@@ -102,12 +160,7 @@ const emit = defineEmits<{
|
|
|
102
160
|
|
|
103
161
|
&__loading {
|
|
104
162
|
padding: 16px 0;
|
|
105
|
-
|
|
106
|
-
.skeleton-row {
|
|
107
|
-
display: flex;
|
|
108
|
-
gap: 16px;
|
|
109
|
-
margin-bottom: 12px;
|
|
110
|
-
}
|
|
163
|
+
.skeleton-row { display: flex; gap: 16px; margin-bottom: 12px; }
|
|
111
164
|
}
|
|
112
165
|
|
|
113
166
|
&__empty {
|
|
@@ -119,12 +172,12 @@ const emit = defineEmits<{
|
|
|
119
172
|
|
|
120
173
|
&__footer {
|
|
121
174
|
padding-top: 8px;
|
|
122
|
-
border-top: 1px solid var(--inactive, #
|
|
175
|
+
border-top: 1px solid var(--inactive, #f3f4f6);
|
|
123
176
|
}
|
|
124
177
|
}
|
|
125
178
|
|
|
126
|
-
.col-item
|
|
127
|
-
.col-qty
|
|
179
|
+
.col-item { flex: 5; display: flex; align-items: center; gap: 8px; }
|
|
180
|
+
.col-qty { flex: 3; display: flex; align-items: center; justify-content: center; gap: 6px; }
|
|
128
181
|
.col-value { flex: 4; display: flex; flex-direction: column; align-items: flex-end; }
|
|
129
182
|
|
|
130
183
|
.mg-cart-item {
|
|
@@ -137,40 +190,37 @@ const emit = defineEmits<{
|
|
|
137
190
|
width: 36px;
|
|
138
191
|
height: 36px;
|
|
139
192
|
object-fit: contain;
|
|
140
|
-
border-radius: 4px;
|
|
141
193
|
}
|
|
142
194
|
|
|
143
195
|
&__name {
|
|
144
196
|
font-size: 13px;
|
|
145
|
-
color: var(--active, #
|
|
197
|
+
color: var(--active, #fff);
|
|
146
198
|
line-height: 1.3;
|
|
147
199
|
}
|
|
148
200
|
}
|
|
149
201
|
|
|
150
202
|
.qty-btn {
|
|
151
|
-
width:
|
|
152
|
-
height:
|
|
203
|
+
width: 26px;
|
|
204
|
+
height: 26px;
|
|
153
205
|
display: flex;
|
|
154
206
|
align-items: center;
|
|
155
207
|
justify-content: center;
|
|
156
208
|
border: 1px solid var(--inactive, #d1d5db);
|
|
157
|
-
border-radius: 4px;
|
|
158
209
|
background: transparent;
|
|
159
|
-
color: var(--active, #
|
|
210
|
+
color: var(--active, #fff);
|
|
160
211
|
cursor: pointer;
|
|
161
|
-
font-size:
|
|
212
|
+
font-size: 16px;
|
|
162
213
|
font-weight: 600;
|
|
214
|
+
transition: all 0.15s;
|
|
163
215
|
|
|
164
|
-
&:hover {
|
|
165
|
-
|
|
166
|
-
color: #4f46e5;
|
|
167
|
-
}
|
|
216
|
+
&:hover:not(:disabled) { border-color: var(--highlight-color, #4f46e5); color: var(--highlight-color, #4f46e5); }
|
|
217
|
+
&:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
168
218
|
}
|
|
169
219
|
|
|
170
220
|
.qty-value {
|
|
171
221
|
font-size: 14px;
|
|
172
222
|
font-weight: 500;
|
|
173
|
-
color: var(--active, #
|
|
223
|
+
color: var(--active, #fff);
|
|
174
224
|
min-width: 20px;
|
|
175
225
|
text-align: center;
|
|
176
226
|
}
|
|
@@ -184,7 +234,7 @@ const emit = defineEmits<{
|
|
|
184
234
|
.final-price {
|
|
185
235
|
font-size: 14px;
|
|
186
236
|
font-weight: 600;
|
|
187
|
-
color: var(--active, #
|
|
237
|
+
color: var(--active, #fff);
|
|
188
238
|
}
|
|
189
239
|
|
|
190
240
|
.clear-btn {
|
|
@@ -197,18 +247,19 @@ const emit = defineEmits<{
|
|
|
197
247
|
font-size: 12px;
|
|
198
248
|
cursor: pointer;
|
|
199
249
|
padding: 4px 0;
|
|
250
|
+
transition: opacity 0.15s;
|
|
200
251
|
|
|
201
|
-
&:hover { opacity: 0.8; }
|
|
252
|
+
&:hover:not(:disabled) { opacity: 0.8; }
|
|
253
|
+
&:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
202
254
|
}
|
|
203
255
|
|
|
204
256
|
.skeleton-block {
|
|
205
257
|
background: var(--inactive, #e5e7eb);
|
|
206
|
-
|
|
207
|
-
animation: pulse 1.5s ease-in-out infinite;
|
|
258
|
+
animation: mg-pulse 1.5s ease-in-out infinite;
|
|
208
259
|
}
|
|
209
260
|
|
|
210
|
-
@keyframes pulse {
|
|
261
|
+
@keyframes mg-pulse {
|
|
211
262
|
0%, 100% { opacity: 1; }
|
|
212
|
-
50% { opacity: 0.
|
|
263
|
+
50% { opacity: 0.4; }
|
|
213
264
|
}
|
|
214
265
|
</style>
|
|
@@ -1,15 +1,37 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* MgCartSummary — cart totals + coupon block.
|
|
4
|
+
*
|
|
5
|
+
* Accepts both raw numbers and pre-formatted strings from the API
|
|
6
|
+
* (e.g. "R$ 49,90", "$ 19.00"). When a formatted string is provided
|
|
7
|
+
* it is displayed as-is; when a raw number is provided it is formatted
|
|
8
|
+
* with the currency symbol.
|
|
9
|
+
*
|
|
10
|
+
* Coupon wiring:
|
|
11
|
+
* - autoWire=true → injects useMgCheckout context (provideMgCheckout in parent)
|
|
12
|
+
* - autoWire=false (default) → emits apply-coupon / remove-coupon events
|
|
13
|
+
*/
|
|
14
|
+
import { computed } from "vue";
|
|
15
|
+
import { injectMgCheckout } from "../../composables/useMgCheckout";
|
|
3
16
|
|
|
4
17
|
const props = defineProps<{
|
|
5
|
-
|
|
6
|
-
|
|
18
|
+
// Totals — accept either a pre-formatted string ("R$ 49,90") or a raw number
|
|
19
|
+
total?: number | string | null;
|
|
20
|
+
originalTotal?: number | string | null;
|
|
21
|
+
// Pre-formatted strings take priority over raw numbers
|
|
22
|
+
formattedTotal?: string | null;
|
|
23
|
+
formattedOriginalTotal?: string | null;
|
|
24
|
+
formattedDiscount?: string | null;
|
|
7
25
|
hasDiscount?: boolean;
|
|
8
26
|
currencySymbol?: string;
|
|
9
|
-
couponInfo?: {
|
|
27
|
+
couponInfo?: { code: string; discount?: number; name?: string } | null;
|
|
10
28
|
couponLoading?: boolean;
|
|
11
29
|
couponError?: string | null;
|
|
12
30
|
disabled?: boolean;
|
|
31
|
+
/** Auto-wire to parent useMgCheckout context */
|
|
32
|
+
autoWire?: boolean;
|
|
33
|
+
/** Show coupon input section */
|
|
34
|
+
showCoupon?: boolean;
|
|
13
35
|
}>();
|
|
14
36
|
|
|
15
37
|
const emit = defineEmits<{
|
|
@@ -17,63 +39,91 @@ const emit = defineEmits<{
|
|
|
17
39
|
"remove-coupon": [];
|
|
18
40
|
}>();
|
|
19
41
|
|
|
20
|
-
const
|
|
42
|
+
const checkout = injectMgCheckout();
|
|
21
43
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
44
|
+
// ── Resolved values (formatted strings preferred) ────────────────────────────
|
|
45
|
+
|
|
46
|
+
function _fmt(raw: number | string | null | undefined, symbol: string): string {
|
|
47
|
+
if (raw == null) return "-";
|
|
48
|
+
if (typeof raw === "string" && /[^\d.,]/.test(raw)) return raw; // already formatted
|
|
49
|
+
const n = parseFloat(String(raw).replace(/[^0-9.]/g, "")) || 0;
|
|
50
|
+
return `${symbol}${n.toFixed(2)}`;
|
|
26
51
|
}
|
|
52
|
+
|
|
53
|
+
const sym = computed(() => props.currencySymbol ?? "$");
|
|
54
|
+
|
|
55
|
+
const displayTotal = computed(() =>
|
|
56
|
+
props.formattedTotal ??
|
|
57
|
+
(props.autoWire ? checkout?.cart.value?.formatted_total_with_discount ?? checkout?.cart.value?.formatted_total : null) ??
|
|
58
|
+
_fmt(props.total, sym.value)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const displayOriginal = computed(() =>
|
|
62
|
+
props.formattedOriginalTotal ??
|
|
63
|
+
(props.autoWire ? checkout?.cart.value?.formatted_original_total : null) ??
|
|
64
|
+
_fmt(props.originalTotal, sym.value)
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const displayDiscount = computed(() =>
|
|
68
|
+
props.formattedDiscount ??
|
|
69
|
+
(props.autoWire ? checkout?.cart.value?.formatted_total_discount : null) ??
|
|
70
|
+
null
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const showDiscount = computed(() =>
|
|
74
|
+
props.autoWire
|
|
75
|
+
? checkout?.hasDiscountFlag.value
|
|
76
|
+
: (props.hasDiscount ?? false)
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const activeCoupon = computed(() =>
|
|
80
|
+
props.autoWire ? checkout?.couponInfo.value : props.couponInfo
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const couponLoading = computed(() =>
|
|
84
|
+
props.autoWire ? checkout?.couponLoading.value : (props.couponLoading ?? false)
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const couponError = computed(() =>
|
|
88
|
+
props.autoWire ? checkout?.couponError.value : props.couponError
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const showCoupon = computed(() => props.showCoupon !== false);
|
|
27
92
|
</script>
|
|
28
93
|
|
|
29
94
|
<template>
|
|
30
95
|
<div class="mg-cart-summary">
|
|
31
|
-
<!-- Coupon
|
|
32
|
-
<div class="mg-cart-summary__coupon">
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
</div>
|
|
43
|
-
<div v-else class="coupon-form">
|
|
44
|
-
<input
|
|
45
|
-
v-model="couponInput"
|
|
46
|
-
type="text"
|
|
47
|
-
:placeholder="$t?.('checkout.coupon.placeholder') ?? 'Discount code'"
|
|
48
|
-
:disabled="disabled || couponLoading"
|
|
49
|
-
class="coupon-input"
|
|
50
|
-
@keyup.enter="handleApply"
|
|
51
|
-
/>
|
|
52
|
-
<button
|
|
53
|
-
class="coupon-btn"
|
|
54
|
-
:disabled="!couponInput.trim() || disabled || couponLoading"
|
|
55
|
-
@click="handleApply"
|
|
56
|
-
>
|
|
57
|
-
<span v-if="couponLoading" class="spinner"></span>
|
|
58
|
-
<template v-else>{{ $t?.("checkout.coupon.apply") ?? "Apply" }}</template>
|
|
59
|
-
</button>
|
|
60
|
-
</div>
|
|
61
|
-
<small v-if="couponError" class="coupon-error">{{ couponError }}</small>
|
|
96
|
+
<!-- Coupon -->
|
|
97
|
+
<div v-if="showCoupon" class="mg-cart-summary__coupon">
|
|
98
|
+
<MgCouponInput
|
|
99
|
+
:coupon-info="activeCoupon"
|
|
100
|
+
:loading="couponLoading"
|
|
101
|
+
:error="couponError"
|
|
102
|
+
:disabled="disabled"
|
|
103
|
+
:auto-wire="autoWire"
|
|
104
|
+
@apply-coupon="(code) => emit('apply-coupon', code)"
|
|
105
|
+
@remove-coupon="emit('remove-coupon')"
|
|
106
|
+
/>
|
|
62
107
|
</div>
|
|
63
108
|
|
|
64
109
|
<!-- Totals -->
|
|
65
110
|
<div class="mg-cart-summary__totals">
|
|
66
|
-
<div v-if="
|
|
111
|
+
<div v-if="showDiscount" class="total-row">
|
|
67
112
|
<span>{{ $t?.("checkout.summary.original") ?? "Subtotal" }}</span>
|
|
68
|
-
<span class="original-total">{{
|
|
113
|
+
<span class="original-total">{{ displayOriginal }}</span>
|
|
69
114
|
</div>
|
|
70
|
-
|
|
115
|
+
|
|
116
|
+
<div v-if="showDiscount" class="total-row discount-row">
|
|
71
117
|
<span>{{ $t?.("checkout.summary.discount") ?? "Discount" }}</span>
|
|
72
|
-
<span class="discount-value"
|
|
118
|
+
<span class="discount-value">
|
|
119
|
+
<template v-if="displayDiscount">-{{ displayDiscount }}</template>
|
|
120
|
+
<template v-else-if="activeCoupon?.discount">-{{ activeCoupon.discount }}%</template>
|
|
121
|
+
</span>
|
|
73
122
|
</div>
|
|
123
|
+
|
|
74
124
|
<div class="total-row total-row--final">
|
|
75
125
|
<span>{{ $t?.("checkout.summary.total") ?? "Total" }}</span>
|
|
76
|
-
<span class="final-total">{{
|
|
126
|
+
<span class="final-total">{{ displayTotal }}</span>
|
|
77
127
|
</div>
|
|
78
128
|
</div>
|
|
79
129
|
</div>
|
|
@@ -93,78 +143,12 @@ function handleApply() {
|
|
|
93
143
|
}
|
|
94
144
|
}
|
|
95
145
|
|
|
96
|
-
.coupon-form {
|
|
97
|
-
display: flex;
|
|
98
|
-
gap: 8px;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.coupon-input {
|
|
102
|
-
flex: 1;
|
|
103
|
-
padding: 8px 12px;
|
|
104
|
-
border: 1px solid var(--inactive, #d1d5db);
|
|
105
|
-
border-radius: 6px;
|
|
106
|
-
font-size: 13px;
|
|
107
|
-
background: transparent;
|
|
108
|
-
color: var(--active, #111827);
|
|
109
|
-
|
|
110
|
-
&::placeholder { color: var(--inactive, #9ca3af); }
|
|
111
|
-
&:focus { outline: none; border-color: #4f46e5; }
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.coupon-btn {
|
|
115
|
-
padding: 8px 16px;
|
|
116
|
-
border: none;
|
|
117
|
-
border-radius: 6px;
|
|
118
|
-
background: #4f46e5;
|
|
119
|
-
color: #fff;
|
|
120
|
-
font-size: 13px;
|
|
121
|
-
font-weight: 500;
|
|
122
|
-
cursor: pointer;
|
|
123
|
-
white-space: nowrap;
|
|
124
|
-
|
|
125
|
-
&:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
126
|
-
&:hover:not(:disabled) { background: #4338ca; }
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.coupon-applied {
|
|
130
|
-
display: flex;
|
|
131
|
-
align-items: center;
|
|
132
|
-
gap: 8px;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
.coupon-badge {
|
|
136
|
-
display: inline-flex;
|
|
137
|
-
padding: 4px 10px;
|
|
138
|
-
border-radius: 4px;
|
|
139
|
-
background: rgba(79, 70, 229, 0.1);
|
|
140
|
-
color: #4f46e5;
|
|
141
|
-
font-size: 12px;
|
|
142
|
-
font-weight: 600;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.coupon-remove {
|
|
146
|
-
background: none;
|
|
147
|
-
border: none;
|
|
148
|
-
color: var(--inactive, #6b7280);
|
|
149
|
-
cursor: pointer;
|
|
150
|
-
padding: 2px;
|
|
151
|
-
|
|
152
|
-
&:hover { color: #ee3831; }
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
.coupon-error {
|
|
156
|
-
display: block;
|
|
157
|
-
color: #ee3831;
|
|
158
|
-
font-size: 12px;
|
|
159
|
-
margin-top: 4px;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
146
|
.total-row {
|
|
163
147
|
display: flex;
|
|
164
148
|
justify-content: space-between;
|
|
165
149
|
align-items: center;
|
|
166
150
|
padding: 6px 0;
|
|
167
|
-
font-size:
|
|
151
|
+
font-size: 14px;
|
|
168
152
|
color: var(--inactive, #6b7280);
|
|
169
153
|
|
|
170
154
|
&--final {
|
|
@@ -173,32 +157,11 @@ function handleApply() {
|
|
|
173
157
|
border-top: 1px solid var(--inactive, #e5e7eb);
|
|
174
158
|
font-size: 16px;
|
|
175
159
|
font-weight: 700;
|
|
176
|
-
color: var(--active, #
|
|
160
|
+
color: var(--active, #fff);
|
|
177
161
|
}
|
|
178
162
|
}
|
|
179
163
|
|
|
180
|
-
.original-total {
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
.discount-value {
|
|
185
|
-
color: #22c55e;
|
|
186
|
-
font-weight: 500;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
.final-total {
|
|
190
|
-
color: var(--active, #111827);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.spinner {
|
|
194
|
-
width: 14px;
|
|
195
|
-
height: 14px;
|
|
196
|
-
border: 2px solid rgba(255, 255, 255, 0.3);
|
|
197
|
-
border-top-color: #fff;
|
|
198
|
-
border-radius: 50%;
|
|
199
|
-
animation: spin 0.6s linear infinite;
|
|
200
|
-
display: inline-block;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
164
|
+
.original-total { text-decoration: line-through; }
|
|
165
|
+
.discount-value { color: #22c55e; font-weight: 500; }
|
|
166
|
+
.final-total { color: var(--active, #fff); }
|
|
204
167
|
</style>
|