@mundogamernetwork/shared-ui 1.7.0 → 1.8.1
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/keys/KeyBrowser.vue +16 -10
- package/components/playtest/video/RecordedSessionPlayer.vue +40 -5
- package/components/playtest/video/VideoPlaytestCapture.vue +38 -2
- package/composables/usePlaytestVideoRecording.ts +106 -3
- package/error.vue +7 -7
- package/locales/de.json +1 -1
- package/locales/en.json +1 -1
- package/locales/pt-BR.json +1 -1
- package/locales/ro.json +1 -1
- package/package.json +3 -1
- package/pages/key-campaigns/redeem-key.vue +3 -0
- package/pages/presskit/game/[slug].vue +7 -1
- /package/components/errors/{401.vue → Error401.vue} +0 -0
- /package/components/errors/{403.vue → Error403.vue} +0 -0
- /package/components/errors/{404.vue → Error404.vue} +0 -0
- /package/components/errors/{500.vue → Error500.vue} +0 -0
- /package/components/errors/{503.vue → Error503.vue} +0 -0
- /package/components/errors/{504.vue → Error504.vue} +0 -0
|
@@ -147,12 +147,16 @@ function handleRequestKey(pool: KeyPool) {
|
|
|
147
147
|
|
|
148
148
|
// ─── Notification prompt ──────────────────────────────────────────────────────
|
|
149
149
|
|
|
150
|
+
// account-management/notification-preferences is the canonical, cross-product
|
|
151
|
+
// preferences endpoint (network-accounts, community-frontend, mobile all use
|
|
152
|
+
// it). Called with no mg_network_system_id so the row is global — one opt-in
|
|
153
|
+
// covers key_campaigns alerts on TV/community/agency alike, matching how the
|
|
154
|
+
// rest of the notification-preferences system already behaves.
|
|
150
155
|
async function _checkNotifPrompt() {
|
|
151
156
|
try {
|
|
152
|
-
const res = await httpService.get('/
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (res.data?.data === null) {
|
|
157
|
+
const res = await httpService.get('/account-management/notification-preferences')
|
|
158
|
+
const category = res.data?.data?.categories?.find((c: any) => c.category === 'key_campaigns')
|
|
159
|
+
if (category && category.is_set === false) {
|
|
156
160
|
notifPromptVisible.value = true
|
|
157
161
|
}
|
|
158
162
|
} catch {
|
|
@@ -163,12 +167,14 @@ async function _checkNotifPrompt() {
|
|
|
163
167
|
async function enableNotifAlerts() {
|
|
164
168
|
notifSaving.value = true
|
|
165
169
|
try {
|
|
166
|
-
await httpService.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
await httpService.post('/account-management/notification-preferences', {
|
|
171
|
+
preferences: [{
|
|
172
|
+
category: 'key_campaigns',
|
|
173
|
+
enabled: true,
|
|
174
|
+
channel_email: true,
|
|
175
|
+
channel_push: true,
|
|
176
|
+
channel_in_app: true,
|
|
177
|
+
}],
|
|
172
178
|
})
|
|
173
179
|
} catch {
|
|
174
180
|
// ignore — user can configure later in settings
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// Playback + clickable marker timeline for a Video Playtest recording.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
import { ref } from "vue";
|
|
3
|
+
// Safari plays HLS natively; every other browser needs hls.js (MediaSource
|
|
4
|
+
// Extensions) — falls back to a direct-link note only if neither path works
|
|
5
|
+
// (very old browsers without MSE support).
|
|
6
|
+
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
|
|
7
|
+
import Hls from "hls.js";
|
|
7
8
|
|
|
8
9
|
const props = defineProps<{
|
|
9
10
|
playbackUrl: string | null;
|
|
@@ -14,6 +15,41 @@ const props = defineProps<{
|
|
|
14
15
|
const videoEl = ref<HTMLVideoElement | null>(null);
|
|
15
16
|
const playbackError = ref(false);
|
|
16
17
|
|
|
18
|
+
let hls: Hls | null = null;
|
|
19
|
+
|
|
20
|
+
function destroyPlayer() {
|
|
21
|
+
if (hls) {
|
|
22
|
+
hls.destroy();
|
|
23
|
+
hls = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setupPlayback() {
|
|
28
|
+
destroyPlayer();
|
|
29
|
+
playbackError.value = false;
|
|
30
|
+
|
|
31
|
+
const video = videoEl.value;
|
|
32
|
+
if (!video || !props.playbackUrl) return;
|
|
33
|
+
|
|
34
|
+
if (Hls.isSupported()) {
|
|
35
|
+
hls = new Hls();
|
|
36
|
+
hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
37
|
+
if (data.fatal) playbackError.value = true;
|
|
38
|
+
});
|
|
39
|
+
hls.loadSource(props.playbackUrl);
|
|
40
|
+
hls.attachMedia(video);
|
|
41
|
+
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
42
|
+
// Safari and other browsers with native HLS support.
|
|
43
|
+
video.src = props.playbackUrl;
|
|
44
|
+
} else {
|
|
45
|
+
playbackError.value = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onMounted(setupPlayback);
|
|
50
|
+
watch(() => props.playbackUrl, setupPlayback);
|
|
51
|
+
onBeforeUnmount(destroyPlayer);
|
|
52
|
+
|
|
17
53
|
function seekTo(seconds: number) {
|
|
18
54
|
if (videoEl.value) {
|
|
19
55
|
videoEl.value.currentTime = seconds;
|
|
@@ -46,7 +82,6 @@ function severityClass(severity?: string | null): string {
|
|
|
46
82
|
<template v-else>
|
|
47
83
|
<video
|
|
48
84
|
ref="videoEl"
|
|
49
|
-
:src="playbackUrl"
|
|
50
85
|
controls
|
|
51
86
|
class="recorded-session-player__video"
|
|
52
87
|
@error="playbackError = true"
|
|
@@ -16,6 +16,8 @@ const emit = defineEmits<{
|
|
|
16
16
|
const rec = usePlaytestVideoRecording(props.campaignId);
|
|
17
17
|
|
|
18
18
|
const markerDraft = ref("");
|
|
19
|
+
const submitted = ref(false);
|
|
20
|
+
const retryingMarkers = ref(false);
|
|
19
21
|
const step = computed<"record" | "review" | "upload" | "done">(() => {
|
|
20
22
|
if (rec.recordedBlob.value && !rec.recordingId.value) return "review";
|
|
21
23
|
if (rec.recordingId.value) return "upload";
|
|
@@ -30,7 +32,23 @@ function addMarker() {
|
|
|
30
32
|
|
|
31
33
|
async function submit() {
|
|
32
34
|
const result = await rec.submitVideoResponse();
|
|
33
|
-
if (result)
|
|
35
|
+
if (!result) return;
|
|
36
|
+
submitted.value = true;
|
|
37
|
+
if (rec.markerSubmitErrors.value === 0) emit("submitted");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function retryMarkers() {
|
|
41
|
+
retryingMarkers.value = true;
|
|
42
|
+
try {
|
|
43
|
+
await rec.retryPendingMarkers();
|
|
44
|
+
if (rec.markerSubmitErrors.value === 0) emit("submitted");
|
|
45
|
+
} finally {
|
|
46
|
+
retryingMarkers.value = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function continueAfterSubmit() {
|
|
51
|
+
emit("submitted");
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
function formatSeconds(s: number): string {
|
|
@@ -97,7 +115,7 @@ function formatSeconds(s: number): string {
|
|
|
97
115
|
</template>
|
|
98
116
|
|
|
99
117
|
<!-- Upload done, waiting on provider processing — submit -->
|
|
100
|
-
<template v-else-if="step === 'upload'">
|
|
118
|
+
<template v-else-if="step === 'upload' && !submitted">
|
|
101
119
|
<p class="video-capture__hint">
|
|
102
120
|
{{ $t("playtest.video_capture.processing_hint", "Your recording is uploaded and processing. Submit when you're ready — we'll finish confirming it's ready in the background.") }}
|
|
103
121
|
</p>
|
|
@@ -107,6 +125,23 @@ function formatSeconds(s: number): string {
|
|
|
107
125
|
: $t("playtest.video_capture.submit", "Submit response") }}
|
|
108
126
|
</button>
|
|
109
127
|
</template>
|
|
128
|
+
|
|
129
|
+
<!-- Response submitted, but one or more markers failed to save — offer a retry instead of silently dropping them -->
|
|
130
|
+
<template v-else-if="submitted && rec.markerSubmitErrors.value > 0">
|
|
131
|
+
<p class="video-capture__hint video-capture__error">
|
|
132
|
+
{{ $t("playtest.video_capture.marker_save_failed", "Response submitted, but {v} note(s) failed to save.", { v: rec.markerSubmitErrors.value }) }}
|
|
133
|
+
</p>
|
|
134
|
+
<div class="video-capture__actions">
|
|
135
|
+
<button type="button" class="btn primary" :disabled="retryingMarkers" @click="retryMarkers">
|
|
136
|
+
{{ retryingMarkers
|
|
137
|
+
? $t("playtest.video_capture.retrying", "Retrying…")
|
|
138
|
+
: $t("playtest.video_capture.retry_markers", "Retry saving notes") }}
|
|
139
|
+
</button>
|
|
140
|
+
<button type="button" class="btn" @click="continueAfterSubmit">
|
|
141
|
+
{{ $t("playtest.video_capture.continue_anyway", "Continue anyway") }}
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
</template>
|
|
110
145
|
</div>
|
|
111
146
|
</template>
|
|
112
147
|
|
|
@@ -130,6 +165,7 @@ function formatSeconds(s: number): string {
|
|
|
130
165
|
|
|
131
166
|
&__actions {
|
|
132
167
|
display: flex;
|
|
168
|
+
gap: 8px;
|
|
133
169
|
}
|
|
134
170
|
|
|
135
171
|
&__recording {
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
* audio only — a real limitation, not an oversight), duration capped
|
|
19
19
|
* server-side (config playtest.video.max_duration_seconds).
|
|
20
20
|
*/
|
|
21
|
+
type PendingMarker = { timestamp_seconds: number; comment: string; severity?: string };
|
|
22
|
+
|
|
21
23
|
export function usePlaytestVideoRecording(campaignId: number) {
|
|
22
24
|
const capturing = ref(false);
|
|
23
25
|
const uploading = ref(false);
|
|
@@ -29,12 +31,45 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
29
31
|
const recording = ref<PlaytestRecording | null>(null);
|
|
30
32
|
const recordedBlob = ref<Blob | null>(null);
|
|
31
33
|
const recordedDurationSeconds = ref(0);
|
|
32
|
-
const markers = ref<
|
|
34
|
+
const markers = ref<PendingMarker[]>([]);
|
|
35
|
+
// Markers a submitted response still needs posted — set when the post-submit
|
|
36
|
+
// loop hits an error partway through, so the UI can offer a retry instead of
|
|
37
|
+
// silently dropping the remainder (see storeRecordingMarker loop below).
|
|
38
|
+
const markerSubmitErrors = ref(0);
|
|
33
39
|
|
|
34
40
|
let mediaRecorder: MediaRecorder | null = null;
|
|
35
41
|
let chunks: BlobPart[] = [];
|
|
36
42
|
let captureStartedAt = 0;
|
|
37
43
|
|
|
44
|
+
// Markers only ever live in this Vue ref + localStorage until they're
|
|
45
|
+
// posted — a crash between adding a marker and a successful submit loses
|
|
46
|
+
// nothing beyond that point (the recording Blob itself is still
|
|
47
|
+
// memory-only and can't survive a reload; that's a separate, harder
|
|
48
|
+
// problem this doesn't attempt to solve).
|
|
49
|
+
const storageKey = `mgn_playtest_pending_markers_${campaignId}`;
|
|
50
|
+
|
|
51
|
+
function persistMarkers() {
|
|
52
|
+
try {
|
|
53
|
+
if (markers.value.length) {
|
|
54
|
+
localStorage.setItem(storageKey, JSON.stringify(markers.value));
|
|
55
|
+
} else {
|
|
56
|
+
localStorage.removeItem(storageKey);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// localStorage unavailable (private mode / quota) — best-effort only.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Restores markers left over from a previous capture on this campaign that never made it to a submit. */
|
|
64
|
+
function restorePendingMarkers(): PendingMarker[] {
|
|
65
|
+
try {
|
|
66
|
+
const raw = localStorage.getItem(storageKey);
|
|
67
|
+
return raw ? (JSON.parse(raw) as PendingMarker[]) : [];
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
38
73
|
function resetError() {
|
|
39
74
|
genericError.value = null;
|
|
40
75
|
}
|
|
@@ -50,6 +85,12 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
50
85
|
|
|
51
86
|
async function startCapture() {
|
|
52
87
|
resetError();
|
|
88
|
+
// A previous capture on this campaign may have left markers behind
|
|
89
|
+
// (crash/reload before a successful submit) — recover them rather than
|
|
90
|
+
// silently starting from an empty list.
|
|
91
|
+
const leftover = restorePendingMarkers();
|
|
92
|
+
if (leftover.length) markers.value = leftover;
|
|
93
|
+
|
|
53
94
|
try {
|
|
54
95
|
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
|
|
55
96
|
chunks = [];
|
|
@@ -80,6 +121,7 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
80
121
|
/** Adds a marker anchored to the current elapsed recording time. */
|
|
81
122
|
function markNow(comment: string, severity?: string) {
|
|
82
123
|
markers.value.push({ timestamp_seconds: elapsedSeconds(), comment, severity });
|
|
124
|
+
persistMarkers();
|
|
83
125
|
}
|
|
84
126
|
|
|
85
127
|
/** Requests the upload slot and uploads the recorded blob directly to the video provider. */
|
|
@@ -118,6 +160,60 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
118
160
|
}
|
|
119
161
|
}
|
|
120
162
|
|
|
163
|
+
let lastSubmittedResponseId: number | null = null;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Marker timestamps are wall-clock elapsed time from when capture started,
|
|
167
|
+
* not the real position in the final encoded file — any delay before
|
|
168
|
+
* MediaRecorder actually starts encoding, or a hiccup mid-recording, drifts
|
|
169
|
+
* them apart. Once the provider confirms the real duration (post-submit,
|
|
170
|
+
* since submit only succeeds once the recording is 'ready'), scale every
|
|
171
|
+
* marker proportionally against the wall-clock duration we measured — a
|
|
172
|
+
* practical correction, not frame-perfect, but far closer than raw elapsed
|
|
173
|
+
* seconds once the two durations diverge.
|
|
174
|
+
*/
|
|
175
|
+
function correctForDrift(realDurationSeconds: number | null | undefined) {
|
|
176
|
+
if (!realDurationSeconds || !recordedDurationSeconds.value) return;
|
|
177
|
+
const scale = realDurationSeconds / recordedDurationSeconds.value;
|
|
178
|
+
if (!isFinite(scale) || scale <= 0 || scale === 1) return;
|
|
179
|
+
markers.value = markers.value.map((m) => ({
|
|
180
|
+
...m,
|
|
181
|
+
timestamp_seconds: Math.min(realDurationSeconds, Math.max(0, Math.round(m.timestamp_seconds * scale))),
|
|
182
|
+
}));
|
|
183
|
+
persistMarkers();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Posts whatever's left in markers.value for responseId, one at a time.
|
|
188
|
+
* A failure on one marker does NOT abort the rest — each is independent
|
|
189
|
+
* feedback, so losing one shouldn't cost the others. Successfully-posted
|
|
190
|
+
* markers are removed from the pending list (and localStorage) as they
|
|
191
|
+
* succeed; anything left after this call can be retried via
|
|
192
|
+
* retryPendingMarkers() instead of being silently dropped.
|
|
193
|
+
*/
|
|
194
|
+
async function postPendingMarkers(responseId: number) {
|
|
195
|
+
lastSubmittedResponseId = responseId;
|
|
196
|
+
const remaining: PendingMarker[] = [];
|
|
197
|
+
let failures = 0;
|
|
198
|
+
for (const m of markers.value) {
|
|
199
|
+
try {
|
|
200
|
+
await storeRecordingMarker(responseId, m as any);
|
|
201
|
+
} catch {
|
|
202
|
+
remaining.push(m);
|
|
203
|
+
failures++;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
markers.value = remaining;
|
|
207
|
+
markerSubmitErrors.value = failures;
|
|
208
|
+
persistMarkers();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Retries any markers that failed to post during submitVideoResponse(). */
|
|
212
|
+
async function retryPendingMarkers() {
|
|
213
|
+
if (!lastSubmittedResponseId || !markers.value.length) return;
|
|
214
|
+
await postPendingMarkers(lastSubmittedResponseId);
|
|
215
|
+
}
|
|
216
|
+
|
|
121
217
|
/**
|
|
122
218
|
* Submits the response once the backend confirms the recording is ready.
|
|
123
219
|
* Processing is async (the provider's webhook updates our DB after
|
|
@@ -135,6 +231,7 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
135
231
|
return null;
|
|
136
232
|
}
|
|
137
233
|
resetError();
|
|
234
|
+
markerSubmitErrors.value = 0;
|
|
138
235
|
submitting.value = true;
|
|
139
236
|
try {
|
|
140
237
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
@@ -144,9 +241,13 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
144
241
|
recording_id: recordingId.value,
|
|
145
242
|
...meta,
|
|
146
243
|
});
|
|
147
|
-
|
|
148
|
-
await
|
|
244
|
+
try {
|
|
245
|
+
const fetched = await fetchRecording(data.id);
|
|
246
|
+
correctForDrift(fetched.data.data?.duration_seconds ?? null);
|
|
247
|
+
} catch {
|
|
248
|
+
// Non-fatal — fall back to uncorrected (wall-clock) timestamps.
|
|
149
249
|
}
|
|
250
|
+
await postPendingMarkers(data.id);
|
|
150
251
|
return data;
|
|
151
252
|
} catch (e: any) {
|
|
152
253
|
if (attempt === maxAttempts) throw e;
|
|
@@ -179,11 +280,13 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
179
280
|
recordedBlob,
|
|
180
281
|
recordedDurationSeconds,
|
|
181
282
|
markers,
|
|
283
|
+
markerSubmitErrors,
|
|
182
284
|
startCapture,
|
|
183
285
|
stopCapture,
|
|
184
286
|
markNow,
|
|
185
287
|
uploadRecording,
|
|
186
288
|
submitVideoResponse,
|
|
289
|
+
retryPendingMarkers,
|
|
187
290
|
loadRecording,
|
|
188
291
|
};
|
|
189
292
|
}
|
package/error.vue
CHANGED
|
@@ -51,13 +51,13 @@ onUnmounted(() => {
|
|
|
51
51
|
|
|
52
52
|
<template>
|
|
53
53
|
<NuxtLayout>
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
<
|
|
60
|
-
<
|
|
54
|
+
<Error401 v-if="statusCodeError === HttpStatusCode.Unauthorized" />
|
|
55
|
+
<Error403 v-else-if="statusCodeError === HttpStatusCode.Forbidden" />
|
|
56
|
+
<Error404 v-else-if="statusCodeError === HttpStatusCode.NotFound" />
|
|
57
|
+
<Error500 v-else-if="statusCodeError === HttpStatusCode.InternalServerError" />
|
|
58
|
+
<Error503 v-else-if="statusCodeError === HttpStatusCode.ServiceUnavailable" />
|
|
59
|
+
<Error504 v-else-if="statusCodeError === HttpStatusCode.GatewayTimeout" />
|
|
60
|
+
<Error500 v-else />
|
|
61
61
|
|
|
62
62
|
<p v-if="isRecoverableError" class="recovery-line">
|
|
63
63
|
{{ $t("errors.recovery_hint") }}
|
package/locales/de.json
CHANGED
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
"description_optional": "Beschreibung (optional)",
|
|
152
152
|
"description_placeholder": "Erzähl uns etwas über deinen Kanal...",
|
|
153
153
|
"sending": "Wird gesendet...",
|
|
154
|
-
"request_sent": "Anfrage gesendet!
|
|
154
|
+
"request_sent": "Anfrage gesendet! Wir informieren dich per E-Mail, sobald sie geprüft wurde.",
|
|
155
155
|
"error_loading": "Kampagnendaten konnten nicht geladen werden.",
|
|
156
156
|
"error_request": "Anfrage konnte nicht gesendet werden. Bitte erneut versuchen.",
|
|
157
157
|
"error_reveal": "Schlüssel konnte nicht enthüllt werden.",
|
package/locales/en.json
CHANGED
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
"description_optional": "Description (optional)",
|
|
152
152
|
"description_placeholder": "Tell us a bit about your channel or content plans...",
|
|
153
153
|
"sending": "Sending...",
|
|
154
|
-
"request_sent": "Request sent!
|
|
154
|
+
"request_sent": "Request sent! We'll email you once it's reviewed.",
|
|
155
155
|
"error_loading": "Failed to load campaign data.",
|
|
156
156
|
"error_request": "Could not submit request. Please try again.",
|
|
157
157
|
"error_reveal": "Could not reveal key. Please try again.",
|
package/locales/pt-BR.json
CHANGED
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
"description_optional": "Descrição (opcional)",
|
|
152
152
|
"description_placeholder": "Conte um pouco sobre seu canal ou planos de conteúdo...",
|
|
153
153
|
"sending": "Enviando...",
|
|
154
|
-
"request_sent": "Solicitação enviada!
|
|
154
|
+
"request_sent": "Solicitação enviada! Você será avisado por e-mail assim que for revisada.",
|
|
155
155
|
"error_loading": "Falha ao carregar dados da campanha.",
|
|
156
156
|
"error_request": "Não foi possível enviar a solicitação. Tente novamente.",
|
|
157
157
|
"error_reveal": "Não foi possível revelar a chave. Tente novamente.",
|
package/locales/ro.json
CHANGED
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
"description_optional": "Descriere (opțional)",
|
|
152
152
|
"description_placeholder": "Spune-ne puțin despre canalul tău...",
|
|
153
153
|
"sending": "Se trimite...",
|
|
154
|
-
"request_sent": "Cerere trimisă!
|
|
154
|
+
"request_sent": "Cerere trimisă! Te vom anunța prin e-mail după ce este analizată.",
|
|
155
155
|
"error_loading": "Eroare la încărcarea datelor campaniei.",
|
|
156
156
|
"error_request": "Nu s-a putut trimite cererea. Încearcă din nou.",
|
|
157
157
|
"error_reveal": "Nu s-a putut dezvălui cheia.",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mundogamernetwork/shared-ui",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"@pinia-plugin-persistedstate/nuxt": ">=1.0.0",
|
|
32
32
|
"@pinia/nuxt": ">=0.5.0",
|
|
33
33
|
"axios": ">=1.0.0",
|
|
34
|
+
"hls.js": ">=1.5.0",
|
|
34
35
|
"laravel-echo": ">=1.15.0",
|
|
35
36
|
"nuxt": ">=3.13.0",
|
|
36
37
|
"pinia": ">=2.1.0",
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"@pinia-plugin-persistedstate/nuxt": "^1.2.1",
|
|
42
43
|
"@pinia/nuxt": "^0.5.5",
|
|
43
44
|
"axios": "^1.18.1",
|
|
45
|
+
"hls.js": "^1.6.16",
|
|
44
46
|
"laravel-echo": "^2.3.7",
|
|
45
47
|
"nuxt": "^3.15.4",
|
|
46
48
|
"pinia": "^2.3.1",
|
|
@@ -203,6 +203,9 @@ function goBack() {
|
|
|
203
203
|
<div v-else-if="success" class="state success">
|
|
204
204
|
<MGIcon icon="check-circle" size="3x" />
|
|
205
205
|
<p>{{ $t("keys.campaigns.request_sent") }}</p>
|
|
206
|
+
<button class="btn" @click="goBack">
|
|
207
|
+
{{ $t("keys.campaigns.back_to_campaign") }}
|
|
208
|
+
</button>
|
|
206
209
|
</div>
|
|
207
210
|
|
|
208
211
|
<div v-else class="form-content">
|
|
@@ -214,6 +214,7 @@ const navSections = computed(() => {
|
|
|
214
214
|
if (awards.value.length) s.push({ id: 'awards', label: 'kit.press.awards' });
|
|
215
215
|
if (team.value.length) s.push({ id: 'team', label: 'kit.press.team' });
|
|
216
216
|
if (hasEcosystem.value) s.push({ id: 'events', label: 'kit.press.showcases' });
|
|
217
|
+
if (k.game_id && keyPoolsVisible.value) s.push({ id: 'review-key', label: 'kit.press.request_review_key' });
|
|
217
218
|
if (k.press_contact_email) s.push({ id: 'contact', label: 'kit.press.contact' });
|
|
218
219
|
return s;
|
|
219
220
|
});
|
|
@@ -338,6 +339,11 @@ useHead(() => ({
|
|
|
338
339
|
<span v-else class="kit-btn kit-btn-primary kit-btn-disabled" :title="$t('kit.press.no_assets')">{{ $t('kit.press.no_assets') }}</span>
|
|
339
340
|
</template>
|
|
340
341
|
<a v-if="kit.press_contact_email" class="kit-btn kit-btn-ghost" :href="`mailto:${kit.press_contact_email}`" @click="track('contact')">{{ $t('kit.press.request_key') }}</a>
|
|
342
|
+
<!-- Real UnifiedKeyRequestController-backed flow (KeyBrowser below) — only
|
|
343
|
+
offered when the kit is actually linked to a game with open pools, same
|
|
344
|
+
gate as the section itself, so this never links to an empty/broken state. -->
|
|
345
|
+
<a v-if="kit.game_id" class="kit-btn kit-btn-ghost" href="#review-key" @click.prevent="scrollToSection('review-key')">{{ $t('kit.press.request_review_key') }}</a>
|
|
346
|
+
<a class="kit-btn kit-btn-ghost" :href="`${agencyBase}/podcast-guest`" target="_blank" rel="noopener" @click="track('click')">{{ $t('kit.press.request_interview') }}</a>
|
|
341
347
|
</div>
|
|
342
348
|
</div>
|
|
343
349
|
</div>
|
|
@@ -492,7 +498,7 @@ useHead(() => ({
|
|
|
492
498
|
</section>
|
|
493
499
|
|
|
494
500
|
<!-- Key pools embed — shown when game has active pools -->
|
|
495
|
-
<section v-if="kit.game_id" v-show="keyPoolsVisible" class="kit-block kit-keys-section">
|
|
501
|
+
<section v-if="kit.game_id" v-show="keyPoolsVisible" id="review-key" class="kit-block kit-keys-section">
|
|
496
502
|
<h2 class="kit-keys-title">{{ $t('kit.press.get_key_title') }}</h2>
|
|
497
503
|
<p class="kit-keys-desc">{{ $t('kit.press.get_key_desc') }}</p>
|
|
498
504
|
<KeyBrowser
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|