@mundogamernetwork/shared-ui 1.2.6 → 1.2.8

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.
@@ -0,0 +1,623 @@
1
+ <template>
2
+ <div id="bundle-editor">
3
+ <div class="header">
4
+ <h3>{{ $t("media_kit.bundles.title") }}</h3>
5
+ <button class="btn-add" v-if="!showForm" @click="openNewForm">
6
+ + {{ $t("media_kit.bundles.add_bundle") }}
7
+ </button>
8
+ </div>
9
+
10
+ <p class="section-desc">{{ $t("media_kit.bundles.description") }}</p>
11
+
12
+ <!-- Bundle Form -->
13
+ <div v-if="showForm" class="bundle-form">
14
+ <div class="form-group">
15
+ <label>{{ $t("media_kit.bundles.bundle_name") }}</label>
16
+ <input v-model="form.name" type="text" :placeholder="$t('media_kit.bundles.name_placeholder')" />
17
+ </div>
18
+
19
+ <div class="form-group">
20
+ <label>{{ $t("media_kit.bundles.bundle_description") }}</label>
21
+ <textarea v-model="form.description" rows="2" :placeholder="$t('media_kit.bundles.description_placeholder')"></textarea>
22
+ </div>
23
+
24
+ <!-- Select rate cards to include -->
25
+ <div class="form-group">
26
+ <label>{{ $t("media_kit.bundles.select_services") }}</label>
27
+ <div class="rate-cards-selector">
28
+ <label
29
+ v-for="card in availableRateCards"
30
+ :key="card.id"
31
+ class="card-checkbox"
32
+ :class="{ selected: form.rate_card_ids.includes(card.id) }"
33
+ >
34
+ <input
35
+ type="checkbox"
36
+ :value="card.id"
37
+ v-model="form.rate_card_ids"
38
+ class="hidden-checkbox"
39
+ />
40
+ <div class="card-checkbox-content">
41
+ <span class="card-name">{{ card.content_format?.name || card.label }}</span>
42
+ <span class="card-channel">{{ card.ad_channel?.name }}</span>
43
+ <span class="card-price">{{ formatPrice(card.price_min, card.currency) }}</span>
44
+ </div>
45
+ <div class="check-indicator">
46
+ <MGIcon icon="checkmark" v-if="form.rate_card_ids.includes(card.id)" />
47
+ </div>
48
+ <div v-if="form.rate_card_ids.includes(card.id)" class="card-qty" @click.stop.prevent>
49
+ <span>×</span>
50
+ <input
51
+ type="number"
52
+ min="1"
53
+ :value="qtyFor(card.id)"
54
+ @input="setQty(card.id, Number(($event.target as HTMLInputElement).value))"
55
+ @click.stop
56
+ />
57
+ </div>
58
+ </label>
59
+ </div>
60
+ <p v-if="form.rate_card_ids.length === 0" class="hint-text">
61
+ {{ $t("media_kit.bundles.select_hint") }}
62
+ </p>
63
+ </div>
64
+
65
+ <!-- Pricing -->
66
+ <div class="pricing-row">
67
+ <div class="form-group">
68
+ <label>{{ $t("media_kit.bundles.original_price") }}</label>
69
+ <div class="price-display">
70
+ {{ formatPrice(String(calculatedOriginalPrice), form.currency) }}
71
+ </div>
72
+ <span class="hint-text">{{ $t("media_kit.bundles.original_hint") }}</span>
73
+ </div>
74
+ <div class="form-group">
75
+ <label>{{ $t("media_kit.bundles.bundle_price") }}</label>
76
+ <input v-model="form.bundle_price" type="number" step="0.01" min="0" />
77
+ </div>
78
+ <div class="form-group">
79
+ <label>{{ $t("media_kit.bundles.currency") }}</label>
80
+ <select v-model="form.currency">
81
+ <option value="USD">USD</option>
82
+ <option value="BRL">BRL</option>
83
+ <option value="EUR">EUR</option>
84
+ </select>
85
+ </div>
86
+ </div>
87
+
88
+ <!-- Discount badge -->
89
+ <div v-if="discountPercent > 0" class="discount-preview">
90
+ <span class="discount-badge">-{{ discountPercent }}%</span>
91
+ <span class="discount-label">{{ $t("media_kit.bundles.savings") }}</span>
92
+ </div>
93
+
94
+ <div class="form-row">
95
+ <div class="form-group">
96
+ <label>{{ $t("media_kit.bundles.delivery_days") }}</label>
97
+ <input v-model="form.delivery_time_days" type="number" min="1" />
98
+ </div>
99
+ <div class="form-group">
100
+ <label>{{ $t("media_kit.bundles.is_active") }}</label>
101
+ <label class="switch">
102
+ <input type="checkbox" v-model="form.is_active" />
103
+ <span class="slider"></span>
104
+ </label>
105
+ </div>
106
+ </div>
107
+
108
+ <div class="form-actions">
109
+ <button class="btn-cancel" @click="cancelForm">{{ $t("media_kit.editor.cancel") }}</button>
110
+ <button class="btn-save" @click="saveBundle" :disabled="saving || form.rate_card_ids.length < 2">
111
+ {{ editing ? $t("media_kit.editor.update") : $t("media_kit.editor.save") }}
112
+ </button>
113
+ </div>
114
+ </div>
115
+
116
+ <!-- Bundles List -->
117
+ <div class="bundles-list">
118
+ <div v-for="bundle in bundles" :key="bundle.id" class="bundle-card">
119
+ <div class="bundle-header">
120
+ <div class="bundle-name">{{ bundle.name }}</div>
121
+ <div class="bundle-actions">
122
+ <button @click="editBundle(bundle)" class="btn-edit"><MGIcon icon="edit" /></button>
123
+ <button @click="removeBundle(bundle.id)" class="btn-delete"><MGIcon icon="trash" /></button>
124
+ </div>
125
+ </div>
126
+ <p v-if="bundle.description" class="bundle-desc">{{ bundle.description }}</p>
127
+ <div class="bundle-items">
128
+ <span v-for="item in bundle.rate_cards?.data || bundle.rate_cards || []" :key="item.id" class="bundle-item-tag">
129
+ {{ item.content_format?.name || item.label }}
130
+ </span>
131
+ </div>
132
+ <div class="bundle-pricing">
133
+ <span class="original-price">{{ formatPrice(String(bundle.original_price), bundle.currency) }}</span>
134
+ <span class="bundle-price">{{ formatPrice(String(bundle.bundle_price), bundle.currency) }}</span>
135
+ <span v-if="getBundleDiscount(bundle) > 0" class="discount-badge">-{{ getBundleDiscount(bundle) }}%</span>
136
+ </div>
137
+ <span v-if="!bundle.is_active" class="inactive-badge">{{ $t("media_kit.bundles.inactive") }}</span>
138
+ </div>
139
+ </div>
140
+
141
+ <div v-if="!bundles.length && !showForm" class="empty-state">
142
+ {{ $t("media_kit.bundles.empty") }}
143
+ </div>
144
+ </div>
145
+ </template>
146
+
147
+ <script setup lang="ts">
148
+ import {
149
+ createRateCardBundle,
150
+ updateRateCardBundle,
151
+ deleteRateCardBundle,
152
+ } from "../../services/mediaKitService";
153
+
154
+ const { t } = useI18n();
155
+
156
+ interface RateCard {
157
+ id: number;
158
+ content_format_id: number;
159
+ ad_channel_id: number;
160
+ label?: string;
161
+ price_min: string;
162
+ price_max?: string;
163
+ currency: string;
164
+ delivery_time_days?: number;
165
+ description?: string;
166
+ content_format?: { id: number; name: string };
167
+ ad_channel?: { id: number; name: string };
168
+ }
169
+
170
+ interface Bundle {
171
+ id: number;
172
+ name: string;
173
+ description?: string;
174
+ original_price: number;
175
+ bundle_price: number;
176
+ currency: string;
177
+ delivery_time_days?: number;
178
+ is_active: boolean;
179
+ rate_cards?: { data: RateCard[] } | RateCard[];
180
+ rate_card_ids?: number[];
181
+ }
182
+
183
+ const props = withDefaults(
184
+ defineProps<{
185
+ bundles: Bundle[];
186
+ availableRateCards: RateCard[];
187
+ influencerProInfoId: number;
188
+ isEnabled?: boolean;
189
+ }>(),
190
+ {
191
+ isEnabled: true,
192
+ }
193
+ );
194
+
195
+ const emit = defineEmits<{
196
+ (e: "refresh"): void;
197
+ (e: "error", message: string): void;
198
+ /** Bundles remain a premium feature — host app decides how to react (e.g. show its own paywall modal). */
199
+ (e: "paywall", feature: string): void;
200
+ }>();
201
+
202
+ const showForm = ref(false);
203
+ const editing = ref<number | null>(null);
204
+ const saving = ref(false);
205
+
206
+ const defaultForm = {
207
+ name: '',
208
+ description: '',
209
+ rate_card_ids: [] as number[],
210
+ quantities: {} as Record<number, number>,
211
+ bundle_price: '',
212
+ currency: 'USD',
213
+ delivery_time_days: '',
214
+ is_active: true,
215
+ };
216
+
217
+ const form = ref({ ...defaultForm, quantities: {} });
218
+
219
+ function qtyFor(id: number): number {
220
+ return form.value.quantities[id] || 1;
221
+ }
222
+ function setQty(id: number, value: number) {
223
+ form.value.quantities = { ...form.value.quantities, [id]: Math.max(1, value || 1) };
224
+ }
225
+
226
+ const calculatedOriginalPrice = computed(() => {
227
+ const selected = props.availableRateCards.filter(c => form.value.rate_card_ids.includes(c.id));
228
+ return selected.reduce((sum, c) => sum + Number(c.price_min) * qtyFor(c.id), 0);
229
+ });
230
+
231
+ const discountPercent = computed(() => {
232
+ const original = calculatedOriginalPrice.value;
233
+ const bundle = Number(form.value.bundle_price);
234
+ if (!original || !bundle || bundle >= original) return 0;
235
+ return Math.round(((original - bundle) / original) * 100);
236
+ });
237
+
238
+ function getBundleDiscount(bundle: Bundle): number {
239
+ if (!bundle.original_price || !bundle.bundle_price || bundle.bundle_price >= bundle.original_price) return 0;
240
+ return Math.round(((bundle.original_price - bundle.bundle_price) / bundle.original_price) * 100);
241
+ }
242
+
243
+ const formatPrice = (value: string, currency = 'USD') => {
244
+ return new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(Number(value));
245
+ };
246
+
247
+ function openNewForm() {
248
+ form.value = { ...defaultForm, rate_card_ids: [], quantities: {} };
249
+ editing.value = null;
250
+ showForm.value = true;
251
+ }
252
+
253
+ function cancelForm() {
254
+ showForm.value = false;
255
+ editing.value = null;
256
+ form.value = { ...defaultForm, rate_card_ids: [], quantities: {} };
257
+ }
258
+
259
+ function editBundle(bundle: Bundle) {
260
+ editing.value = bundle.id;
261
+ const cards = Array.isArray(bundle.rate_cards)
262
+ ? bundle.rate_cards
263
+ : bundle.rate_cards?.data || [];
264
+ const items = (bundle as any).items || [];
265
+ const quantities: Record<number, number> = {};
266
+ items.forEach((it: any) => { quantities[it.rate_card_id] = it.quantity || 1; });
267
+ form.value = {
268
+ name: bundle.name,
269
+ description: bundle.description || '',
270
+ rate_card_ids: bundle.rate_card_ids || cards.map(c => c.id),
271
+ quantities,
272
+ bundle_price: String(bundle.bundle_price),
273
+ currency: bundle.currency,
274
+ delivery_time_days: bundle.delivery_time_days ? String(bundle.delivery_time_days) : '',
275
+ is_active: bundle.is_active,
276
+ };
277
+ showForm.value = true;
278
+ }
279
+
280
+ async function saveBundle() {
281
+ if (props.isEnabled === false) {
282
+ emit('paywall', 'rates');
283
+ return;
284
+ }
285
+ saving.value = true;
286
+ try {
287
+ const payload = {
288
+ influencer_professional_info_id: props.influencerProInfoId,
289
+ name: form.value.name,
290
+ description: form.value.description || null,
291
+ items: form.value.rate_card_ids.map(id => ({ rate_card_id: id, quantity: qtyFor(id) })),
292
+ original_price: calculatedOriginalPrice.value,
293
+ bundle_price: Number(form.value.bundle_price),
294
+ currency: form.value.currency,
295
+ delivery_time_days: form.value.delivery_time_days ? Number(form.value.delivery_time_days) : null,
296
+ is_active: form.value.is_active,
297
+ };
298
+
299
+ if (editing.value) {
300
+ await updateRateCardBundle(editing.value, payload);
301
+ } else {
302
+ await createRateCardBundle(payload);
303
+ }
304
+
305
+ cancelForm();
306
+ emit('refresh');
307
+ } catch (error) {
308
+ emit('error', t('media_kit.editor.error_generic'));
309
+ console.error('Error saving bundle:', error);
310
+ } finally {
311
+ saving.value = false;
312
+ }
313
+ }
314
+
315
+ async function removeBundle(id: number) {
316
+ try {
317
+ await deleteRateCardBundle(id);
318
+ emit('refresh');
319
+ } catch (error) {
320
+ emit('error', t('media_kit.editor.error_generic'));
321
+ console.error('Error deleting bundle:', error);
322
+ }
323
+ }
324
+ </script>
325
+
326
+ <style lang="scss" scoped>
327
+ #bundle-editor {
328
+ display: flex;
329
+ flex-direction: column;
330
+ gap: 16px;
331
+
332
+ .header {
333
+ display: flex;
334
+ justify-content: space-between;
335
+ align-items: center;
336
+
337
+ h3 { color: #FFF; font-size: 16px; font-weight: 600; }
338
+
339
+ .btn-add {
340
+ background: #FDB215;
341
+ color: #13161C;
342
+ border: none;
343
+ padding: 8px 16px;
344
+ font-size: 13px;
345
+ font-weight: 500;
346
+ cursor: pointer;
347
+ }
348
+ }
349
+
350
+ .section-desc {
351
+ color: #888;
352
+ font-size: 13px;
353
+ margin: -8px 0 0;
354
+ }
355
+
356
+ .bundle-form {
357
+ background: #1E1E22;
358
+ padding: 20px;
359
+ display: flex;
360
+ flex-direction: column;
361
+ gap: 16px;
362
+
363
+ .form-group {
364
+ display: flex;
365
+ flex-direction: column;
366
+ gap: 6px;
367
+
368
+ label {
369
+ color: #AAA;
370
+ font-size: 12px;
371
+ font-weight: 500;
372
+ }
373
+
374
+ input, select, textarea {
375
+ background: #272930;
376
+ border: 1px solid #333;
377
+ color: #FFF;
378
+ padding: 10px 12px;
379
+ font-size: 13px;
380
+ outline: none;
381
+ &:focus { border-color: #FDB215; }
382
+ }
383
+ }
384
+
385
+ .pricing-row, .form-row {
386
+ display: flex;
387
+ gap: 16px;
388
+ @media (max-width: 576px) { flex-direction: column; }
389
+
390
+ .form-group { flex: 1; }
391
+ }
392
+
393
+ .price-display {
394
+ background: #272930;
395
+ border: 1px solid #333;
396
+ color: #FDB215;
397
+ padding: 10px 12px;
398
+ font-size: 14px;
399
+ font-weight: 600;
400
+ }
401
+
402
+ .hint-text {
403
+ color: #666;
404
+ font-size: 11px;
405
+ }
406
+
407
+ .rate-cards-selector {
408
+ display: flex;
409
+ flex-direction: column;
410
+ gap: 6px;
411
+
412
+ .card-checkbox {
413
+ display: flex;
414
+ align-items: center;
415
+ justify-content: space-between;
416
+ background: #272930;
417
+ border: 1px solid #333;
418
+ padding: 10px 14px;
419
+ cursor: pointer;
420
+ transition: all 0.2s;
421
+
422
+ &.selected {
423
+ border-color: #FDB215;
424
+ background: rgba(253, 178, 21, 0.08);
425
+ }
426
+
427
+ &:hover { border-color: #555; }
428
+
429
+ .hidden-checkbox { display: none; }
430
+
431
+ .card-checkbox-content {
432
+ display: flex;
433
+ gap: 12px;
434
+ align-items: center;
435
+ flex: 1;
436
+
437
+ .card-name { color: #FFF; font-size: 13px; font-weight: 600; }
438
+ .card-channel { color: #888; font-size: 12px; }
439
+ .card-price { color: #FDB215; font-size: 13px; font-weight: 600; margin-left: auto; }
440
+ }
441
+
442
+ .check-indicator {
443
+ width: 20px;
444
+ height: 20px;
445
+ display: flex;
446
+ align-items: center;
447
+ justify-content: center;
448
+ color: #FDB215;
449
+ margin-left: 8px;
450
+ }
451
+ }
452
+ }
453
+
454
+ .discount-preview {
455
+ display: flex;
456
+ align-items: center;
457
+ gap: 8px;
458
+ padding: 8px 12px;
459
+ background: rgba(46, 204, 113, 0.1);
460
+
461
+ .discount-badge {
462
+ background: #27ae60;
463
+ color: #FFF;
464
+ padding: 2px 8px;
465
+ font-size: 12px;
466
+ font-weight: 700;
467
+ }
468
+
469
+ .discount-label {
470
+ color: #27ae60;
471
+ font-size: 13px;
472
+ }
473
+ }
474
+
475
+ .switch {
476
+ position: relative;
477
+ display: inline-block;
478
+ width: 44px;
479
+ height: 24px;
480
+
481
+ input { opacity: 0; width: 0; height: 0; }
482
+
483
+ .slider {
484
+ position: absolute;
485
+ cursor: pointer;
486
+ inset: 0;
487
+ background-color: #333;
488
+ transition: 0.3s;
489
+ &::before {
490
+ content: "";
491
+ position: absolute;
492
+ height: 18px; width: 18px;
493
+ left: 3px; bottom: 3px;
494
+ background-color: white;
495
+ transition: 0.3s;
496
+ }
497
+ }
498
+
499
+ input:checked + .slider { background-color: #FDB215; }
500
+ input:checked + .slider::before { transform: translateX(20px); }
501
+ }
502
+
503
+ .form-actions {
504
+ display: flex;
505
+ justify-content: flex-end;
506
+ gap: 8px;
507
+
508
+ .btn-cancel {
509
+ background: transparent;
510
+ color: #AAA;
511
+ border: 1px solid #333;
512
+ padding: 8px 16px;
513
+ cursor: pointer;
514
+ }
515
+ .btn-save {
516
+ background: #FDB215;
517
+ color: #13161C;
518
+ border: none;
519
+ padding: 8px 20px;
520
+ font-weight: 600;
521
+ cursor: pointer;
522
+ &:disabled { opacity: 0.4; cursor: not-allowed; }
523
+ }
524
+ }
525
+ }
526
+
527
+ .bundles-list {
528
+ display: flex;
529
+ flex-direction: column;
530
+ gap: 12px;
531
+
532
+ .bundle-card {
533
+ background: #191B20;
534
+ padding: 16px 20px;
535
+ display: flex;
536
+ flex-direction: column;
537
+ gap: 8px;
538
+
539
+ .bundle-header {
540
+ display: flex;
541
+ justify-content: space-between;
542
+ align-items: center;
543
+
544
+ .bundle-name { color: #FFF; font-size: 15px; font-weight: 600; }
545
+
546
+ .bundle-actions {
547
+ display: flex;
548
+ gap: 6px;
549
+
550
+ button {
551
+ background: transparent;
552
+ border: none;
553
+ cursor: pointer;
554
+ padding: 4px;
555
+ i { color: #AAA; font-size: 16px; }
556
+ &:hover i { color: #FFF; }
557
+ }
558
+ .btn-delete:hover i { color: #ff4d4f; }
559
+ }
560
+ }
561
+
562
+ .bundle-desc { color: #888; font-size: 13px; margin: 0; }
563
+
564
+ .bundle-items {
565
+ display: flex;
566
+ flex-wrap: wrap;
567
+ gap: 6px;
568
+
569
+ .bundle-item-tag {
570
+ background: rgba(253, 178, 21, 0.12);
571
+ color: #FDB215;
572
+ padding: 3px 10px;
573
+ font-size: 11px;
574
+ font-weight: 500;
575
+ }
576
+ }
577
+
578
+ .bundle-pricing {
579
+ display: flex;
580
+ align-items: center;
581
+ gap: 10px;
582
+ margin-top: 4px;
583
+
584
+ .original-price {
585
+ color: #666;
586
+ font-size: 14px;
587
+ text-decoration: line-through;
588
+ }
589
+
590
+ .bundle-price {
591
+ color: #FDB215;
592
+ font-size: 16px;
593
+ font-weight: 700;
594
+ }
595
+
596
+ .discount-badge {
597
+ background: #27ae60;
598
+ color: #FFF;
599
+ padding: 2px 8px;
600
+ font-size: 11px;
601
+ font-weight: 700;
602
+ }
603
+ }
604
+
605
+ .inactive-badge {
606
+ background: rgba(149, 165, 166, 0.15);
607
+ color: #95a5a6;
608
+ padding: 2px 8px;
609
+ font-size: 11px;
610
+ align-self: flex-start;
611
+ }
612
+ }
613
+ }
614
+
615
+ .empty-state {
616
+ color: #666;
617
+ font-size: 13px;
618
+ text-align: center;
619
+ padding: 24px;
620
+ background: #18181A;
621
+ }
622
+ }
623
+ </style>