@mundogamernetwork/shared-ui 1.4.2 → 1.6.0

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,6 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  import { fetchPublicMediaKit, trackMediaKitEvent } from '../../services/mediaKitService';
3
3
  import MediaKitWallBlock from '../../components/indie-wall/MediaKitWallBlock.vue';
4
+ import MgMediaKitStore from '../../components/MediaKit/MgMediaKitStore.vue';
4
5
 
5
6
  const route = useRoute();
6
7
  const slug = computed(() => route.params.slug as string);
@@ -25,6 +26,15 @@ const template = computed(() => kit.value?.template || 'creator');
25
26
  const platforms = computed(() => kit.value?.platforms ?? []);
26
27
  const totalFollowers = computed(() => kit.value?.total_followers ?? 0);
27
28
  const isPremium = computed(() => kit.value?.is_premium ?? false);
29
+ const coverUrl = computed(() => kit.value?.cover_url || streamer.value?.cover || null);
30
+ const portfolioImages = computed(() => kit.value?.portfolio_images ?? streamer.value?.portfolio_images ?? []);
31
+ const brandLogos = computed(() => kit.value?.brand_logos ?? streamer.value?.brand_logos ?? []);
32
+ const creatorStore = computed(() => kit.value?.creator_store ?? null);
33
+ const customLinks = computed(() => kit.value?.custom_links ?? streamer.value?.custom_links ?? []);
34
+
35
+ const coverStyle = computed(() => coverUrl.value
36
+ ? { backgroundImage: `linear-gradient(180deg, rgba(5,6,10,.14), rgba(5,6,10,.92)), url("${String(coverUrl.value).replace(/"/g, '\\"')}")` }
37
+ : undefined);
28
38
 
29
39
  const PLATFORM_LABEL: Record<string, string> = {
30
40
  instagram: 'Instagram', tiktok: 'TikTok', youtube: 'YouTube', twitch: 'Twitch', x: 'X',
@@ -109,7 +119,7 @@ function onDownload() {
109
119
  <div v-else-if="error || !kit" class="kit-state"><p>{{ $t('kit.media.not_available') }}</p></div>
110
120
 
111
121
  <template v-else>
112
- <div class="mk-cover" />
122
+ <div class="mk-cover" :class="{ 'has-image': coverUrl }" :style="coverStyle" />
113
123
 
114
124
  <div class="kit-wrap mk-sections">
115
125
  <header class="mk-header">
@@ -118,6 +128,7 @@ function onDownload() {
118
128
 
119
129
  <div class="mk-hid">
120
130
  <h1>{{ streamer.name }}</h1>
131
+ <p v-if="streamer.custom_headline" class="mk-headline">{{ streamer.custom_headline }}</p>
121
132
  <div class="mk-badges">
122
133
  <span v-if="streamer.platform" class="mk-badge plat">{{ streamer.platform }}</span>
123
134
  <span v-if="streamer.tier && streamer.tier !== 'free'" class="mk-badge tier">{{ String(streamer.tier).toUpperCase() }}</span>
@@ -132,6 +143,25 @@ function onDownload() {
132
143
  </div>
133
144
  </header>
134
145
 
146
+ <!-- Personal link hub: replaces a separate link-in-bio service. -->
147
+ <nav class="mk-link-hub no-print" data-sec="links" :aria-label="$t('kit.media.quick_links')">
148
+ <a v-for="(link, index) in customLinks" :key="`${link.url}-${index}`" class="mk-link" :class="{ 'mk-link--primary': index === 0 }" :href="link.url" target="_blank" rel="noopener" @click="track('click')">
149
+ <span>{{ link.label }}</span><b>↗</b>
150
+ </a>
151
+ <a v-if="streamer.contact_email" class="mk-link mk-link--primary" :href="`mailto:${streamer.contact_email}`" @click="track('contact')">
152
+ <span>{{ $t('kit.media.work_with_me') }}</span><b>↗</b>
153
+ </a>
154
+ <a v-if="creatorStore" class="mk-link" href="#store" @click="track('click')">
155
+ <span>{{ $t('kit.media.visit_store') }}</span><b>↓</b>
156
+ </a>
157
+ <a v-if="userId" class="mk-link" href="#support" @click="track('click')">
158
+ <span>{{ $t('kit.media.visit_mural') }}</span><b>↓</b>
159
+ </a>
160
+ <a v-for="s in socials" :key="s.platform" class="mk-link mk-link--social" :href="s.url" target="_blank" rel="noopener" @click="track('click')">
161
+ <span>{{ PLATFORM_LABEL[s.platform] || s.platform }}</span><b>↗</b>
162
+ </a>
163
+ </nav>
164
+
135
165
  <!-- Reach / multi-platform followers -->
136
166
  <section v-if="platforms.length" class="kit-block" data-sec="reach">
137
167
  <h2>{{ $t('kit.media.reach') }}</h2>
@@ -151,6 +181,24 @@ function onDownload() {
151
181
  </div>
152
182
  </section>
153
183
 
184
+ <!-- Mundo Gamer creator store -->
185
+ <section v-if="creatorStore" id="store" class="kit-block kit-card mk-store-block" data-sec="store">
186
+ <MgMediaKitStore :store="creatorStore" :contact-email="streamer.contact_email" @click="track('click')" />
187
+ </section>
188
+
189
+ <!-- Creator-selected portfolio and brand imagery -->
190
+ <section v-if="portfolioImages.length || brandLogos.length" class="kit-block" data-sec="portfolio">
191
+ <h2>{{ $t('kit.media.portfolio') }}</h2>
192
+ <div v-if="portfolioImages.length" class="mk-portfolio">
193
+ <a v-for="(image, index) in portfolioImages" :key="image" :href="image" target="_blank" rel="noopener" @click="track('click')">
194
+ <img :src="image" :alt="`${streamer.name} — ${index + 1}`" />
195
+ </a>
196
+ </div>
197
+ <div v-if="brandLogos.length" class="mk-brand-logos">
198
+ <img v-for="logo in brandLogos" :key="logo" :src="logo" :alt="$t('kit.media.partner_brand')" />
199
+ </div>
200
+ </section>
201
+
154
202
  <!-- TV streaming metrics -->
155
203
  <section v-if="stats" class="kit-block" data-sec="metrics">
156
204
  <h2>{{ $t('kit.media.key_metrics') }}</h2>
@@ -262,18 +310,8 @@ function onDownload() {
262
310
  </div>
263
311
  </section>
264
312
 
265
- <!-- Social links -->
266
- <section v-if="socials.length" class="kit-block no-print" data-sec="connect">
267
- <h2>{{ $t('kit.media.connect') }}</h2>
268
- <div class="mk-socials">
269
- <a v-for="s in socials" :key="s.platform" class="mk-soc" :href="s.url" target="_blank" rel="noopener" @click="track('click')">
270
- {{ s.platform }}
271
- </a>
272
- </div>
273
- </section>
274
-
275
313
  <!-- IndieWall / mural — self-contained widget (own fetch, realtime, stepper) -->
276
- <section v-if="userId" class="kit-block" data-sec="support">
314
+ <section v-if="userId" id="support" class="kit-block" data-sec="support">
277
315
  <MediaKitWallBlock :user-id="userId" />
278
316
  </section>
279
317
 
@@ -308,8 +346,10 @@ function onDownload() {
308
346
 
309
347
  /* Cover */
310
348
  .mk-cover {
311
- height: 200px;
349
+ height: 280px;
312
350
  background: linear-gradient(120deg, var(--kit-accent-2, #6b15ac) 0%, #2a0e4e 50%, var(--kit-bg) 100%);
351
+ background-position: center;
352
+ background-size: cover;
313
353
  position: relative;
314
354
  border-bottom: 1px solid var(--kit-line);
315
355
 
@@ -319,44 +359,59 @@ function onDownload() {
319
359
  inset: 0;
320
360
  background: radial-gradient(circle at 80% 20%, rgba(168, 85, 247, .35), transparent 60%);
321
361
  }
362
+
363
+ &.has-image::after {
364
+ background: linear-gradient(180deg, rgba(5, 6, 10, .04), rgba(5, 6, 10, .52));
365
+ }
322
366
  }
323
367
 
324
368
  /* Header */
325
369
  .mk-header {
326
- margin-top: -70px;
370
+ margin: -82px auto 0;
327
371
  position: relative;
328
372
  z-index: 2;
329
373
  display: flex;
330
- gap: 24px;
331
- align-items: flex-end;
332
- flex-wrap: wrap;
374
+ flex-direction: column;
375
+ align-items: center;
376
+ max-width: 760px;
377
+ text-align: center;
333
378
  }
334
379
  .mk-avatar {
335
- width: 140px;
336
- height: 140px;
380
+ width: 156px;
381
+ height: 156px;
337
382
  border: 3px solid var(--kit-accent);
338
383
  object-fit: cover;
339
384
  flex-shrink: 0;
340
385
  background: var(--kit-surface-2);
386
+ box-shadow: 0 20px 52px rgba(0, 0, 0, .42);
341
387
  }
342
388
  .mk-avatar-ph {
343
389
  background: linear-gradient(135deg, var(--kit-accent-2, #6b15ac), var(--kit-accent));
344
390
  }
345
391
  .mk-hid {
346
- flex: 1;
347
- min-width: 260px;
348
- padding-bottom: 6px;
392
+ width: 100%;
393
+ padding-top: 18px;
349
394
 
350
395
  h1 {
351
396
  font-family: 'Rajdhani', sans-serif;
352
- font-size: 40px;
397
+ font-size: clamp(36px, 7vw, 54px);
353
398
  line-height: 1.05;
399
+ margin: 0;
354
400
  }
355
401
  }
402
+ .mk-headline {
403
+ margin: 8px auto 0;
404
+ max-width: 620px;
405
+ color: var(--kit-accent);
406
+ font-family: 'Rajdhani', sans-serif;
407
+ font-size: 20px;
408
+ font-weight: 600;
409
+ }
356
410
  .mk-badges {
357
411
  display: flex;
412
+ justify-content: center;
358
413
  gap: 8px;
359
- margin: 8px 0;
414
+ margin: 12px 0;
360
415
  flex-wrap: wrap;
361
416
  }
362
417
  .mk-badge {
@@ -374,13 +429,60 @@ function onDownload() {
374
429
  .mk-bio {
375
430
  color: #c4c4d2;
376
431
  font-size: 15px;
377
- max-width: 60ch;
378
- margin-top: 6px;
432
+ line-height: 1.65;
433
+ max-width: 66ch;
434
+ margin: 6px auto 0;
379
435
  }
380
436
  .mk-actions {
381
437
  display: flex;
438
+ justify-content: center;
439
+ flex-wrap: wrap;
382
440
  gap: 10px;
383
- padding-bottom: 8px;
441
+ padding-top: 18px;
442
+ }
443
+
444
+ /* Link-in-bio hub */
445
+ .mk-link-hub {
446
+ display: grid;
447
+ grid-template-columns: 1fr 1fr;
448
+ gap: 10px;
449
+ width: min(100%, 720px);
450
+ margin: 30px auto 2px;
451
+ }
452
+ .mk-link {
453
+ display: flex;
454
+ align-items: center;
455
+ justify-content: space-between;
456
+ gap: 16px;
457
+ min-height: 54px;
458
+ padding: 0 18px;
459
+ border: 1px solid var(--kit-line);
460
+ background: color-mix(in srgb, var(--kit-surface) 92%, transparent);
461
+ color: var(--kit-text);
462
+ font-size: 14px;
463
+ font-weight: 700;
464
+ text-decoration: none;
465
+ transition: transform .16s ease, border-color .16s ease, background .16s ease;
466
+
467
+ &:hover {
468
+ transform: translateY(-2px);
469
+ border-color: var(--kit-accent);
470
+ background: var(--kit-surface-2);
471
+ }
472
+
473
+ b { color: var(--kit-accent); font-size: 16px; }
474
+
475
+ &--primary {
476
+ grid-column: 1 / -1;
477
+ background: var(--kit-accent);
478
+ border-color: var(--kit-accent);
479
+ color: var(--kit-accent-ink);
480
+
481
+ b { color: var(--kit-accent-ink); }
482
+ &:hover { background: var(--kit-accent); filter: brightness(1.06); }
483
+ }
484
+
485
+ &--social { min-height: 46px; color: var(--kit-muted); }
384
486
  }
385
487
 
386
488
  /* Buttons */
@@ -434,6 +536,29 @@ function onDownload() {
434
536
  }
435
537
  }
436
538
  }
539
+ .mk-store-block { padding: 24px; scroll-margin-top: 24px; }
540
+ .mk-portfolio {
541
+ display: grid;
542
+ grid-template-columns: repeat(3, minmax(0, 1fr));
543
+ gap: 10px;
544
+
545
+ a { display: block; overflow: hidden; aspect-ratio: 4 / 3; border: 1px solid var(--kit-line); background: var(--kit-surface); }
546
+ img { width: 100%; height: 100%; object-fit: cover; transition: transform .2s ease; }
547
+ a:hover img { transform: scale(1.03); }
548
+ }
549
+ .mk-brand-logos {
550
+ display: flex;
551
+ align-items: center;
552
+ justify-content: center;
553
+ flex-wrap: wrap;
554
+ gap: 16px;
555
+ margin-top: 16px;
556
+ padding: 18px;
557
+ border: 1px solid var(--kit-line);
558
+ background: var(--kit-surface);
559
+
560
+ img { width: 112px; height: 54px; object-fit: contain; filter: grayscale(1); opacity: .8; }
561
+ }
437
562
  .mk-period {
438
563
  color: var(--kit-muted);
439
564
  font-size: 13px;
@@ -768,16 +893,19 @@ function onDownload() {
768
893
  .mk-sections { display: flex; flex-direction: column; }
769
894
  .mk-sections > .mk-header { order: 0; }
770
895
  .mk-sections > .mk-footer { order: 100; }
771
- .mk-sections > [data-sec="reach"] { order: 1; }
772
- .mk-sections > [data-sec="metrics"] { order: 2; }
773
- .mk-sections > [data-sec="audience"] { order: 3; }
774
- .mk-sections > [data-sec="styles"] { order: 4; }
775
- .mk-sections > [data-sec="games"] { order: 5; }
776
- .mk-sections > [data-sec="rates"] { order: 6; }
777
- .mk-sections > [data-sec="brands"] { order: 7; }
778
- .mk-sections > [data-sec="schedule"] { order: 8; }
779
- .mk-sections > [data-sec="connect"] { order: 9; }
780
- .mk-sections > [data-sec="support"] { order: 10; }
896
+ .mk-sections > [data-sec="links"] { order: 1; }
897
+ .mk-sections > [data-sec="reach"] { order: 2; }
898
+ .mk-sections > [data-sec="store"] { order: 3; }
899
+ .mk-sections > [data-sec="portfolio"] { order: 4; }
900
+ .mk-sections > [data-sec="metrics"] { order: 5; }
901
+ .mk-sections > [data-sec="audience"] { order: 6; }
902
+ .mk-sections > [data-sec="styles"] { order: 7; }
903
+ .mk-sections > [data-sec="games"] { order: 8; }
904
+ .mk-sections > [data-sec="rates"] { order: 9; }
905
+ .mk-sections > [data-sec="brands"] { order: 10; }
906
+ .mk-sections > [data-sec="schedule"] { order: 11; }
907
+ .mk-sections > [data-sec="connect"] { order: 12; }
908
+ .mk-sections > [data-sec="support"] { order: 13; scroll-margin-top: 24px; }
781
909
 
782
910
  [data-template="performance"] {
783
911
  .mk-sections > [data-sec="metrics"] { order: 1; }
@@ -803,7 +931,20 @@ function onDownload() {
803
931
  .mk-cols2, .mk-rates, .mk-bundles { grid-template-columns: 1fr; }
804
932
  .mk-brands { grid-template-columns: repeat(3, 1fr); }
805
933
  .mk-schedule { grid-template-columns: repeat(4, 1fr); }
806
- .mk-header { flex-direction: column; align-items: flex-start; }
934
+ .mk-portfolio { grid-template-columns: 1fr 1fr; }
935
+ }
936
+
937
+ @media (max-width: 560px) {
938
+ .kit-wrap { padding-inline: 14px; }
939
+ .mk-cover { height: 230px; }
940
+ .mk-avatar { width: 132px; height: 132px; }
941
+ .mk-header { margin-top: -68px; }
942
+ .mk-link-hub { grid-template-columns: 1fr; }
943
+ .mk-link--primary { grid-column: auto; }
944
+ .mk-actions { width: 100%; }
945
+ .kit-btn { flex: 1; justify-content: center; }
946
+ .mk-portfolio { grid-template-columns: 1fr; }
947
+ .mk-store-block { padding: 16px; }
807
948
  }
808
949
 
809
950
  @media print {
@@ -811,7 +952,7 @@ function onDownload() {
811
952
  }
812
953
 
813
954
  // ── Scroll unlock (body has overflow-y: hidden globally in some frontends) ───
814
- :global(body):has(.pk-page) {
955
+ :global(body):has(.kit-media) {
815
956
  overflow-y: auto !important;
816
957
  max-height: none !important;
817
958
  }
@@ -421,6 +421,20 @@ export const revealKey = (requestId: number) => {
421
421
  return authClient.get(`/public/key-requests/${requestId}/reveal`)
422
422
  }
423
423
 
424
+ /**
425
+ * Explicit opt-in to key campaign alerts — same endpoint used by
426
+ * KeyBrowser.vue's notification prompt (GET/PUT /public/user/notification-preferences).
427
+ */
428
+ export const enableKeyCampaignNotifications = () => {
429
+ return authClient.put("/public/user/notification-preferences", {
430
+ category: "key_campaigns",
431
+ enabled: true,
432
+ channel_email: true,
433
+ channel_push: true,
434
+ channel_in_app: true,
435
+ })
436
+ }
437
+
424
438
  /**
425
439
  * Fetch required actions for a campaign.
426
440
  */
@@ -63,6 +63,8 @@ export interface SubmitBaselineResponsePayload {
63
63
  time_on_task_seconds?: number
64
64
  device?: string
65
65
  os?: string
66
+ // Video Playtest only — id of a ready PlaytestRecording (see requestRecordingUploadUrl()).
67
+ recording_id?: number
66
68
  }
67
69
 
68
70
  export interface PlaytestResponse {
@@ -414,3 +416,64 @@ export const fetchMyPlaytestResponses = (status?: 'submitted' | 'accepted' | 're
414
416
  httpService.get<PaginatedResponse<PlaytestResponse>>('/playtest/my-responses', {
415
417
  params: { status, per_page: perPage, page },
416
418
  })
419
+
420
+ // ---------------------------------------------------------------------------
421
+ // Game access delivery + liability waiver (accepted responses only)
422
+ // ---------------------------------------------------------------------------
423
+
424
+ export interface PlaytestAccessGrant {
425
+ id: number
426
+ access_type: 'key' | 'link'
427
+ revealed: boolean
428
+ revealed_at: string | null
429
+ key_code?: string
430
+ external_link?: string
431
+ }
432
+
433
+ /** Null when the response was accepted without a configured access grant. */
434
+ export const fetchAccessGrant = (responseId: number) =>
435
+ httpService.get<{ data: PlaytestAccessGrant | null }>(`/playtest/responses/${responseId}/access-grant`)
436
+
437
+ /** Sends (or resends) the liability-waiver OTP to the tester's account email. */
438
+ export const initiateAccessGrantWaiver = (responseId: number) =>
439
+ httpService.post<{ data: { document_id: number; status: string } }>(`/playtest/responses/${responseId}/waiver/initiate`)
440
+
441
+ /** Verifies the OTP code; on success the response includes the revealed key/link. */
442
+ export const verifyAccessGrantWaiver = (responseId: number, otpCode: string) =>
443
+ httpService.post<{ data: PlaytestAccessGrant }>(`/playtest/responses/${responseId}/waiver/verify`, { otp_code: otpCode })
444
+
445
+ // ---------------------------------------------------------------------------
446
+ // Video Playtest — recording upload + timestamped markers ("minutagem")
447
+ // ---------------------------------------------------------------------------
448
+
449
+ export interface PlaytestRecording {
450
+ id: number
451
+ status: 'uploading' | 'processing' | 'ready' | 'failed'
452
+ duration_seconds: number | null
453
+ playback_url: string | null
454
+ external_id: string
455
+ markers?: PlaytestRecordingMarker[]
456
+ }
457
+
458
+ export interface PlaytestRecordingMarker {
459
+ id: number
460
+ timestamp_seconds: number
461
+ comment: string
462
+ severity: 'blocker' | 'major' | 'minor' | 'note' | null
463
+ created_by_user_id: number
464
+ }
465
+
466
+ /** Requests a direct-upload URL from the video provider — the browser uploads straight to it, never through this API. */
467
+ export const requestRecordingUploadUrl = (campaignId: number) =>
468
+ httpService.post<{ data: { recording_id: number; upload_url: string } }>(
469
+ `/playtest/campaigns/${campaignId}/recording/upload-url`,
470
+ )
471
+
472
+ /** Poll this until status === 'ready' (set by the provider's webhook once processing finishes). */
473
+ export const fetchRecording = (responseId: number) =>
474
+ httpService.get<{ data: PlaytestRecording | null }>(`/playtest/responses/${responseId}/recording`)
475
+
476
+ export const storeRecordingMarker = (
477
+ responseId: number,
478
+ payload: { timestamp_seconds: number; comment: string; severity?: 'blocker' | 'major' | 'minor' | 'note' },
479
+ ) => httpService.post<{ data: PlaytestRecordingMarker }>(`/playtest/responses/${responseId}/recording/markers`, payload)