@mundogamernetwork/shared-ui 1.1.4 → 1.1.9

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.
@@ -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: CartItem[];
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
- <div v-if="loading" class="mg-cart-items__loading">
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"></div>
29
- <div class="skeleton-block" style="width: 20%; height: 14px"></div>
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
- <div v-else-if="items.length === 0" class="mg-cart-items__empty">
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 items"
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">{{ item.item?.name ?? "Product" }}</span>
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="emit('remove-item', item.item_id, item.item_type)">-</button>
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="emit('add-item', item.item_id, item.item_type)">+</button>
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
- <span v-if="item.discount > 0" class="original-price">
64
- {{ currencySymbol ?? "$" }}{{ (item.price * item.quantity).toFixed(2) }}
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
- {{ currencySymbol ?? "$" }}{{ (item.price_with_discount * item.quantity).toFixed(2) }}
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="emit('clear-all')">
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" /><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" />
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, #e5e7eb);
175
+ border-top: 1px solid var(--inactive, #f3f4f6);
123
176
  }
124
177
  }
125
178
 
126
- .col-item { flex: 5; display: flex; align-items: center; gap: 8px; }
127
- .col-qty { flex: 3; display: flex; align-items: center; justify-content: center; gap: 6px; }
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, #111827);
197
+ color: var(--active, #fff);
146
198
  line-height: 1.3;
147
199
  }
148
200
  }
149
201
 
150
202
  .qty-btn {
151
- width: 24px;
152
- height: 24px;
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, #111827);
210
+ color: var(--active, #fff);
160
211
  cursor: pointer;
161
- font-size: 14px;
212
+ font-size: 16px;
162
213
  font-weight: 600;
214
+ transition: all 0.15s;
163
215
 
164
- &:hover {
165
- border-color: #4f46e5;
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, #111827);
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, #111827);
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
- border-radius: 4px;
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.5; }
263
+ 50% { opacity: 0.4; }
213
264
  }
214
265
  </style>
@@ -1,15 +1,37 @@
1
1
  <script setup lang="ts">
2
- import { ref } from "vue";
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
- total: number | string;
6
- originalTotal?: number | string;
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?: { name: string; discount: number; code: string } | null;
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 couponInput = ref("");
42
+ const checkout = injectMgCheckout();
21
43
 
22
- function handleApply() {
23
- if (couponInput.value.trim()) {
24
- emit("apply-coupon", couponInput.value.trim());
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 Input -->
32
- <div class="mg-cart-summary__coupon">
33
- <div v-if="couponInfo" class="coupon-applied">
34
- <span class="coupon-badge">
35
- {{ couponInfo.code }} (-{{ couponInfo.discount }}%)
36
- </span>
37
- <button class="coupon-remove" @click="emit('remove-coupon')">
38
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
39
- <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
40
- </svg>
41
- </button>
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="hasDiscount" class="total-row">
111
+ <div v-if="showDiscount" class="total-row">
67
112
  <span>{{ $t?.("checkout.summary.original") ?? "Subtotal" }}</span>
68
- <span class="original-total">{{ currencySymbol ?? "$" }}{{ Number(originalTotal).toFixed(2) }}</span>
113
+ <span class="original-total">{{ displayOriginal }}</span>
69
114
  </div>
70
- <div v-if="hasDiscount" class="total-row discount-row">
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">-{{ currencySymbol ?? "$" }}{{ (Number(originalTotal) - Number(total)).toFixed(2) }}</span>
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">{{ currencySymbol ?? "$" }}{{ Number(total).toFixed(2) }}</span>
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: 13px;
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, #111827);
160
+ color: var(--active, #fff);
177
161
  }
178
162
  }
179
163
 
180
- .original-total {
181
- text-decoration: line-through;
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>
@@ -154,7 +154,6 @@ async function handleCheckout() {
154
154
  padding: 16px;
155
155
  background: var(--body-bg-card, #fff);
156
156
  border: 1px solid var(--inactive, #f3f4f6);
157
- border-radius: 8px;
158
157
  }
159
158
 
160
159
  &__actions {
@@ -198,7 +197,6 @@ async function handleCheckout() {
198
197
  width: 100%;
199
198
  height: 48px;
200
199
  border: none;
201
- border-radius: 8px;
202
200
  background: #4f46e5;
203
201
  color: #fff;
204
202
  font-size: 15px;