@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.
- package/components/MediaKit/MgMediaKitLinks.vue +121 -0
- package/components/MediaKit/MgMediaKitStore.vue +107 -0
- package/components/checkout/MgPaymentMethodSelector.vue +3 -1
- package/components/form/SortInput.vue +9 -2
- package/components/keys/KeyBrowser.vue +2 -2
- package/components/playtest/PlaytestResponseFlow.vue +7 -1
- package/components/playtest/access-grant/AccessGrantPanel.vue +80 -0
- package/components/playtest/access-grant/AccessGrantReveal.vue +132 -0
- package/components/playtest/access-grant/AccessGrantWaiver.vue +170 -0
- package/components/playtest/video/RecordedSessionPlayer.vue +162 -0
- package/components/playtest/video/VideoPlaytestCapture.vue +195 -0
- package/components/ui/MgPaymentMethods.vue +20 -0
- package/composables/usePaymentMethods.ts +43 -12
- package/composables/usePlaytestAccessGrant.ts +88 -0
- package/composables/usePlaytestVideoRecording.ts +191 -0
- package/locales/de.json +1 -0
- package/locales/en.json +1 -0
- package/locales/pt-BR.json +1 -0
- package/locales/ro.json +1 -0
- package/nuxt.config.ts +10 -3
- package/package.json +1 -1
- package/pages/key-campaigns/index.vue +7 -6
- package/pages/key-campaigns/redeem-key.vue +36 -0
- package/pages/media-kit/[slug].vue +180 -39
- package/services/campaignService.ts +14 -0
- package/services/playtestTesterService.ts +63 -0
|
@@ -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:
|
|
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
|
|
370
|
+
margin: -82px auto 0;
|
|
327
371
|
position: relative;
|
|
328
372
|
z-index: 2;
|
|
329
373
|
display: flex;
|
|
330
|
-
|
|
331
|
-
align-items:
|
|
332
|
-
|
|
374
|
+
flex-direction: column;
|
|
375
|
+
align-items: center;
|
|
376
|
+
max-width: 760px;
|
|
377
|
+
text-align: center;
|
|
333
378
|
}
|
|
334
379
|
.mk-avatar {
|
|
335
|
-
width:
|
|
336
|
-
height:
|
|
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
|
-
|
|
347
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
378
|
-
|
|
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-
|
|
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="
|
|
772
|
-
.mk-sections > [data-sec="
|
|
773
|
-
.mk-sections > [data-sec="
|
|
774
|
-
.mk-sections > [data-sec="
|
|
775
|
-
.mk-sections > [data-sec="
|
|
776
|
-
.mk-sections > [data-sec="
|
|
777
|
-
.mk-sections > [data-sec="
|
|
778
|
-
.mk-sections > [data-sec="
|
|
779
|
-
.mk-sections > [data-sec="
|
|
780
|
-
.mk-sections > [data-sec="
|
|
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-
|
|
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(.
|
|
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)
|