@mundogamernetwork/shared-ui 1.2.7 → 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,479 @@
1
+ <template>
2
+ <div id="rate-card-editor">
3
+ <div class="header">
4
+ <h3>{{ $t("media_kit.editor.rate_cards_title") }}</h3>
5
+ <button class="btn-add" @click="showForm = true" v-if="!showForm">
6
+ + {{ $t("media_kit.editor.add_rate_card") }}
7
+ </button>
8
+ </div>
9
+
10
+ <div v-if="showForm" class="rate-card-form">
11
+ <div class="form-row">
12
+ <div class="form-group">
13
+ <label>{{ $t("media_kit.editor.content_format") }}</label>
14
+ <select v-model="form.content_format_id" :class="{ 'input-error': rateCardErrors.content_format_id }" @change="delete rateCardErrors.content_format_id" required>
15
+ <option value="">{{ $t("media_kit.editor.select") }}</option>
16
+ <option v-for="format in contentFormats" :key="format.id" :value="format.id">
17
+ {{ format.name }}
18
+ </option>
19
+ </select>
20
+ </div>
21
+ <div class="form-group">
22
+ <label>{{ $t("media_kit.editor.ad_channel") }}</label>
23
+ <select v-model="form.ad_channel_id" :class="{ 'input-error': rateCardErrors.ad_channel_id }" @change="delete rateCardErrors.ad_channel_id" required>
24
+ <option value="">{{ $t("media_kit.editor.select") }}</option>
25
+ <option v-for="channel in adChannels" :key="channel.id" :value="channel.id">
26
+ {{ channel.name }}
27
+ </option>
28
+ </select>
29
+ </div>
30
+ </div>
31
+ <div class="form-row">
32
+ <div class="form-group">
33
+ <label>{{ $t("media_kit.editor.price_min") }}</label>
34
+ <input v-model="form.price_min" type="number" step="0.01" min="0" :class="{ 'input-error': rateCardErrors.price_min }" @input="delete rateCardErrors.price_min" required />
35
+ <span v-if="rateCardErrors.price_min" class="field-error">{{ $t('media_kit.editor.error_required') }}</span>
36
+ </div>
37
+ <div class="form-group">
38
+ <label>{{ $t("media_kit.editor.price_max") }}</label>
39
+ <input v-model="form.price_max" type="number" step="0.01" min="0" />
40
+ </div>
41
+ <div class="form-group">
42
+ <label>{{ $t("media_kit.editor.currency") }}</label>
43
+ <select v-model="form.currency">
44
+ <option value="USD">USD</option>
45
+ <option value="BRL">BRL</option>
46
+ <option value="EUR">EUR</option>
47
+ </select>
48
+ </div>
49
+ </div>
50
+
51
+ <div v-if="showFee" class="mgn-fee-note">
52
+ <div class="fee-row">
53
+ <span class="fee-label">
54
+ {{ $t("media_kit.editor.mgn_fee_label", { percent: commissionPercent, plan: planName }) }}
55
+ </span>
56
+ <span class="fee-amount">- {{ feeAmount }}</span>
57
+ </div>
58
+ <div class="fee-row net">
59
+ <span>{{ $t("media_kit.editor.you_receive") }}</span>
60
+ <strong>{{ netDisplay }}</strong>
61
+ </div>
62
+ <small v-if="commissionPercent > 10" class="fee-upsell">
63
+ {{ $t("media_kit.editor.fee_upsell") }}
64
+ </small>
65
+ </div>
66
+
67
+ <div class="form-row">
68
+ <div class="form-group">
69
+ <label>{{ $t("media_kit.editor.delivery_days") }}</label>
70
+ <input v-model="form.delivery_time_days" type="number" min="1" />
71
+ </div>
72
+ <div class="form-group flex-2">
73
+ <label>{{ $t("media_kit.editor.label") }}</label>
74
+ <input v-model="form.label" type="text" :placeholder="$t('media_kit.editor.label_placeholder')" />
75
+ </div>
76
+ </div>
77
+ <div class="form-group">
78
+ <label>{{ $t("media_kit.editor.description") }}</label>
79
+ <textarea v-model="form.description" rows="3"
80
+ :placeholder="$t('media_kit.editor.description_placeholder')"></textarea>
81
+ </div>
82
+ <div class="form-actions">
83
+ <button class="btn-cancel" @click="cancelForm">{{ $t("media_kit.editor.cancel") }}</button>
84
+ <button class="btn-save" @click="saveRateCard" :disabled="saving">
85
+ {{ editing ? $t("media_kit.editor.update") : $t("media_kit.editor.save") }}
86
+ </button>
87
+ </div>
88
+ </div>
89
+
90
+ <div class="rate-cards-list">
91
+ <div v-for="card in rateCards" :key="card.id" class="rate-card-item">
92
+ <div class="card-info">
93
+ <span class="format">{{ card.content_format?.name || card.label }}</span>
94
+ <span class="channel">{{ card.ad_channel?.name }}</span>
95
+ <span class="price">{{ formatPrice(card.price_min, card.price_max, card.currency) }}</span>
96
+ </div>
97
+ <div class="card-actions">
98
+ <button @click="editCard(card)" class="btn-edit">
99
+ <MGIcon icon="edit" />
100
+ </button>
101
+ <button @click="removeCard(card.id)" class="btn-delete">
102
+ <MGIcon icon="trash" />
103
+ </button>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </template>
109
+
110
+ <script setup lang="ts">
111
+ import { createRateCard, updateRateCard, deleteRateCard } from "../../services/mediaKitService";
112
+
113
+ const { t } = useI18n();
114
+
115
+ interface RateCard {
116
+ id: number;
117
+ content_format_id: number;
118
+ ad_channel_id: number;
119
+ label?: string;
120
+ price_min: string;
121
+ price_max?: string;
122
+ currency: string;
123
+ delivery_time_days?: number;
124
+ description?: string;
125
+ is_active: boolean;
126
+ content_format?: { id: number; name: string };
127
+ ad_channel?: { id: number; name: string };
128
+ }
129
+
130
+ interface LookupItem {
131
+ id: number;
132
+ name: string;
133
+ }
134
+
135
+ const props = withDefaults(
136
+ defineProps<{
137
+ rateCards: RateCard[];
138
+ influencerProInfoId: number;
139
+ contentFormats: LookupItem[];
140
+ adChannels: LookupItem[];
141
+ isEnabled?: boolean;
142
+ /** MGN commission % for the current plan (host app resolves this, e.g. from feature_limits). */
143
+ commissionPercent?: number;
144
+ /** Current plan name, used in the fee label. */
145
+ planName?: string;
146
+ }>(),
147
+ {
148
+ isEnabled: true,
149
+ commissionPercent: 20,
150
+ planName: "Free",
151
+ }
152
+ );
153
+
154
+ const emit = defineEmits<{
155
+ (e: "refresh"): void;
156
+ (e: "error", message: string): void;
157
+ }>();
158
+
159
+ const showForm = ref(false);
160
+ const editing = ref<number | null>(null);
161
+ const saving = ref(false);
162
+
163
+ const defaultForm = {
164
+ content_format_id: '',
165
+ ad_channel_id: '',
166
+ price_min: '',
167
+ price_max: '',
168
+ currency: 'USD',
169
+ delivery_time_days: '',
170
+ label: '',
171
+ description: '',
172
+ };
173
+
174
+ const form = ref({ ...defaultForm });
175
+
176
+ const cancelForm = () => {
177
+ showForm.value = false;
178
+ editing.value = null;
179
+ form.value = { ...defaultForm };
180
+ };
181
+
182
+ const editCard = (card: RateCard) => {
183
+ editing.value = card.id;
184
+ form.value = {
185
+ content_format_id: String(card.content_format_id),
186
+ ad_channel_id: String(card.ad_channel_id),
187
+ price_min: card.price_min,
188
+ price_max: card.price_max || '',
189
+ currency: card.currency,
190
+ delivery_time_days: card.delivery_time_days ? String(card.delivery_time_days) : '',
191
+ label: card.label || '',
192
+ description: card.description || '',
193
+ };
194
+ showForm.value = true;
195
+ };
196
+
197
+ const rateCardErrors = reactive<Record<string, string>>({});
198
+
199
+ const saveRateCard = async () => {
200
+ // Individual rate cards are free on every plan — no paywall gate here.
201
+ // Client-side required field check (HTML required bypassed by @click)
202
+ Object.keys(rateCardErrors).forEach(k => delete rateCardErrors[k]);
203
+ if (!form.value.content_format_id) rateCardErrors.content_format_id = 'required';
204
+ if (!form.value.ad_channel_id) rateCardErrors.ad_channel_id = 'required';
205
+ if (!form.value.price_min && form.value.price_min !== 0) rateCardErrors.price_min = 'required';
206
+ if (Object.keys(rateCardErrors).length) return;
207
+
208
+ saving.value = true;
209
+ try {
210
+ const payload = {
211
+ ...form.value,
212
+ influencer_professional_info_id: props.influencerProInfoId,
213
+ content_format_id: Number(form.value.content_format_id),
214
+ ad_channel_id: Number(form.value.ad_channel_id),
215
+ price_min: Number(form.value.price_min),
216
+ price_max: form.value.price_max ? Number(form.value.price_max) : null,
217
+ delivery_time_days: form.value.delivery_time_days ? Number(form.value.delivery_time_days) : null,
218
+ };
219
+
220
+ if (editing.value) {
221
+ await updateRateCard(editing.value, payload);
222
+ } else {
223
+ await createRateCard(payload);
224
+ }
225
+
226
+ cancelForm();
227
+ emit('refresh');
228
+ } catch (error) {
229
+ emit('error', t('media_kit.editor.error_generic'));
230
+ console.error('Error saving rate card:', error);
231
+ } finally {
232
+ saving.value = false;
233
+ }
234
+ };
235
+
236
+ const removeCard = async (id: number) => {
237
+ try {
238
+ await deleteRateCard(id);
239
+ emit('refresh');
240
+ } catch (error) {
241
+ emit('error', t('media_kit.editor.error_generic'));
242
+ console.error('Error deleting rate card:', error);
243
+ }
244
+ };
245
+
246
+ const formatPrice = (min: string, max?: string, currency = 'USD') => {
247
+ const fmt = (v: string) => new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(Number(v));
248
+ if (max && Number(max) > Number(min)) return `${fmt(min)} - ${fmt(max)}`;
249
+ return fmt(min);
250
+ };
251
+
252
+ // --- MGN fee preview -------------------------------------------------------
253
+ const fmtMoney = (v: number) =>
254
+ new Intl.NumberFormat('en-US', { style: 'currency', currency: form.value.currency || 'USD' }).format(v);
255
+
256
+ const netOf = (v: string): number | null => {
257
+ const n = Number(v);
258
+ if (!n || n <= 0) return null;
259
+ return n * (1 - props.commissionPercent / 100);
260
+ };
261
+
262
+ const showFee = computed(() => Number(form.value.price_min) > 0);
263
+
264
+ const feeAmount = computed(() => {
265
+ const n = Number(form.value.price_min);
266
+ if (!n || n <= 0) return '';
267
+ return fmtMoney(n * (props.commissionPercent / 100));
268
+ });
269
+
270
+ const netDisplay = computed(() => {
271
+ const min = netOf(form.value.price_min);
272
+ if (min == null) return '';
273
+ const max = netOf(form.value.price_max);
274
+ if (max != null && Number(form.value.price_max) > Number(form.value.price_min)) {
275
+ return `${fmtMoney(min)} – ${fmtMoney(max)}`;
276
+ }
277
+ return fmtMoney(min);
278
+ });
279
+ </script>
280
+
281
+ <style lang="scss" scoped>
282
+ #rate-card-editor {
283
+ display: flex;
284
+ flex-direction: column;
285
+ gap: 16px;
286
+
287
+ .mgn-fee-note {
288
+ display: flex;
289
+ flex-direction: column;
290
+ gap: 6px;
291
+ padding: 12px 14px;
292
+ margin-bottom: 4px;
293
+ background: rgba(253, 178, 21, 0.06);
294
+ border: 1px solid rgba(253, 178, 21, 0.18);
295
+ border-left: 3px solid #FDB215;
296
+
297
+ .fee-row {
298
+ display: flex;
299
+ justify-content: space-between;
300
+ align-items: center;
301
+ font-size: 13px;
302
+ color: #B7BAC4;
303
+
304
+ .fee-amount { color: #E36464; }
305
+
306
+ &.net {
307
+ color: #FFF;
308
+ font-size: 14px;
309
+
310
+ strong { color: #3FB37F; }
311
+ }
312
+ }
313
+
314
+ .fee-upsell {
315
+ color: #FDB215;
316
+ font-size: 12px;
317
+ }
318
+ }
319
+
320
+ .header {
321
+ display: flex;
322
+ justify-content: space-between;
323
+ align-items: center;
324
+
325
+ h3 {
326
+ color: #FFF;
327
+ font-size: 16px;
328
+ font-weight: 600;
329
+ }
330
+
331
+ .btn-add {
332
+ background: #FDB215;
333
+ color: #13161C;
334
+ border: none;
335
+ padding: 8px 16px;
336
+ font-size: 13px;
337
+ font-weight: 500;
338
+ cursor: pointer;
339
+ }
340
+ }
341
+
342
+ .rate-card-form {
343
+ background: #1E1E22;
344
+ padding: 16px;
345
+ display: flex;
346
+ flex-direction: column;
347
+ gap: 12px;
348
+
349
+ .form-row {
350
+ display: flex;
351
+ gap: 12px;
352
+
353
+ @media (max-width: 576px) {
354
+ flex-direction: column;
355
+ }
356
+ }
357
+
358
+ .form-group {
359
+ display: flex;
360
+ flex-direction: column;
361
+ gap: 4px;
362
+ flex: 1;
363
+
364
+ &.flex-2 {
365
+ flex: 2;
366
+ }
367
+
368
+ label {
369
+ color: #AAA;
370
+ font-size: 12px;
371
+ }
372
+
373
+ input,
374
+ select,
375
+ textarea {
376
+ background: #272930;
377
+ border: 1px solid #333;
378
+ color: #FFF;
379
+ padding: 8px;
380
+ font-size: 13px;
381
+ outline: none;
382
+
383
+ &:focus {
384
+ border-color: #FDB215;
385
+ }
386
+ }
387
+ }
388
+
389
+ .form-actions {
390
+ display: flex;
391
+ justify-content: flex-end;
392
+ gap: 8px;
393
+
394
+ .btn-cancel {
395
+ background: transparent;
396
+ color: #AAA;
397
+ border: 1px solid #333;
398
+ padding: 8px 16px;
399
+ cursor: pointer;
400
+ }
401
+
402
+ .btn-save {
403
+ background: #FDB215;
404
+ color: #13161C;
405
+ border: none;
406
+ padding: 8px 16px;
407
+ font-weight: 500;
408
+ cursor: pointer;
409
+
410
+ &:disabled {
411
+ opacity: 0.5;
412
+ }
413
+ }
414
+ }
415
+ }
416
+
417
+ .rate-cards-list {
418
+ display: flex;
419
+ flex-direction: column;
420
+ gap: 8px;
421
+
422
+ .rate-card-item {
423
+ display: flex;
424
+ justify-content: space-between;
425
+ align-items: center;
426
+ background: #191B20;
427
+ padding: 12px 16px;
428
+
429
+ .card-info {
430
+ display: flex;
431
+ gap: 16px;
432
+ align-items: center;
433
+
434
+ .format {
435
+ color: #FFF;
436
+ font-size: 14px;
437
+ font-weight: 600;
438
+ }
439
+
440
+ .channel {
441
+ color: #AAA;
442
+ font-size: 12px;
443
+ }
444
+
445
+ .price {
446
+ color: #FDB215;
447
+ font-size: 14px;
448
+ font-weight: 600;
449
+ }
450
+ }
451
+
452
+ .card-actions {
453
+ display: flex;
454
+ gap: 8px;
455
+
456
+ button {
457
+ background: transparent;
458
+ border: none;
459
+ cursor: pointer;
460
+ padding: 4px;
461
+
462
+ i {
463
+ color: #AAA;
464
+ font-size: 16px;
465
+ }
466
+
467
+ &:hover i {
468
+ color: #FFF;
469
+ }
470
+ }
471
+
472
+ .btn-delete:hover i {
473
+ color: #ff4d4f;
474
+ }
475
+ }
476
+ }
477
+ }
478
+ }
479
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.2.7",
3
+ "version": "1.2.8",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -26,10 +26,18 @@ const SUPPORTED_LOCALES = ['en', 'pt-BR', 'es', 'de', 'ro', 'ar-AE'];
26
26
  const routePath = route.path.split('/');
27
27
  const lang = SUPPORTED_LOCALES.find(l => routePath.includes(l)) || 'en';
28
28
 
29
+ // Admin-generated preview links (network-adminV3) pass this so drafts/archived
30
+ // kits render on the same public page without needing to be published first.
31
+ const previewToken = route.query.preview_token as string | undefined;
32
+
29
33
  const { data: kitData, error: fetchError, pending } = await useAsyncData(
30
34
  `press-kit-${slug}`,
31
35
  () => $fetch<any>(`${apiBase}/public/press-kits/${slug}`, {
32
- params: { include: 'assets,videos,credits,awards,quotes', lang },
36
+ params: {
37
+ include: 'assets,videos,credits,awards,quotes',
38
+ lang,
39
+ ...(previewToken ? { preview_token: previewToken } : {}),
40
+ },
33
41
  }),
34
42
  { lazy: false },
35
43
  );
@@ -48,7 +56,7 @@ const loading = pending;
48
56
  const error = computed(() => !!fetchError.value || (!pending.value && !kit.value));
49
57
 
50
58
  onMounted(() => {
51
- if (kit.value) {
59
+ if (kit.value && !previewToken) {
52
60
  trackPressKitEvent(slug, {
53
61
  event_type: 'view',
54
62
  referrer: document.referrer || undefined,
@@ -165,6 +173,7 @@ const moreKits = computed(() => {
165
173
  });
166
174
 
167
175
  function track(type: 'click' | 'download' | 'contact') {
176
+ if (previewToken) return;
168
177
  trackPressKitEvent(slug, { event_type: type, referrer: document.referrer || undefined }).catch(() => {});
169
178
  }
170
179
 
@@ -52,3 +52,50 @@ export const trackMediaKitEvent = (
52
52
  userId: number | string,
53
53
  data: { type?: "view" | "click" | "download" | "contact"; referrer?: string } = {},
54
54
  ) => mk.post(`media-kit/${userId}/track`, data)
55
+
56
+ // ── Authoring (dashboard builder) ──────────────────────────────────────────
57
+ // Shared across every front's media-kit builder (agency, tv, community…). The
58
+ // base URL already ends in /api/v1, so paths are relative (no leading slash).
59
+
60
+ export const fetchRateCards = (params: Record<string, any> = {}) =>
61
+ mk.get("rate-cards", { params })
62
+ export const createRateCard = (data: Record<string, any>) =>
63
+ mk.post("rate-cards", data)
64
+ export const updateRateCard = (id: number, data: Record<string, any>) =>
65
+ mk.put(`rate-cards/${id}`, data)
66
+ export const deleteRateCard = (id: number) =>
67
+ mk.delete(`rate-cards/${id}`)
68
+
69
+ export const fetchRateCardBundles = (params: Record<string, any> = {}) =>
70
+ mk.get("rate-card-bundles", { params })
71
+ export const createRateCardBundle = (data: Record<string, any>) =>
72
+ mk.post("rate-card-bundles", data)
73
+ export const updateRateCardBundle = (id: number, data: Record<string, any>) =>
74
+ mk.put(`rate-card-bundles/${id}`, data)
75
+ export const deleteRateCardBundle = (id: number) =>
76
+ mk.delete(`rate-card-bundles/${id}`)
77
+
78
+ export const fetchMediaKitSettings = (influencerProInfoId: number) =>
79
+ mk.get(`media-kit-settings/${influencerProInfoId}`)
80
+ export const createMediaKitSettings = (data: Record<string, any>) =>
81
+ mk.post("media-kit-settings", data)
82
+ export const updateMediaKitSettings = (influencerProInfoId: number, data: Record<string, any>) =>
83
+ mk.put(`media-kit-settings/${influencerProInfoId}`, data)
84
+
85
+ // Uploads a single image (avatar / cover / portfolio / brand logo) → public URL.
86
+ export const uploadMediaKitImage = async (file: File): Promise<string> => {
87
+ const form = new FormData()
88
+ form.append("image", file)
89
+ const res = await mk.post("me/media-kit/upload-image", form, {
90
+ headers: { "Content-Type": "multipart/form-data" },
91
+ })
92
+ return (res?.data as any)?.data?.url as string
93
+ }
94
+
95
+ // Rate card editor dropdown lookups — public, auth-free, shared across every
96
+ // front's builder (mirrors agency's /public/influencer-lookups/* but reachable
97
+ // from any service that registers MediaKitProvider, not just agency-api).
98
+ export const fetchMediaKitContentFormats = () =>
99
+ mk.get("media-kit-lookups/content-formats")
100
+ export const fetchMediaKitAdChannels = () =>
101
+ mk.get("media-kit-lookups/ad-channels")