@mundogamernetwork/shared-ui 1.16.23 → 1.16.25

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.
@@ -4,10 +4,12 @@
4
4
  // role for the (unrelated, accept-time) key/link grant. Renders nothing if
5
5
  // the campaign has no ready build asset at all, so campaigns that don't use
6
6
  // this feature (concept polls, etc.) show no empty panel.
7
- import { onMounted, computed } from "vue";
7
+ import { onMounted, computed, ref } from "vue";
8
8
  import { usePlaytestBuildAccess } from "../../../composables/usePlaytestBuildAccess";
9
9
  import NdaSign from "./NdaSign.vue";
10
10
  import BuildDownload from "./BuildDownload.vue";
11
+ import ReportProblemForm from "./ReportProblemForm.vue";
12
+ import type { StoreBuildProblemReportPayload } from "../../../services/playtestTesterService";
11
13
 
12
14
  const props = defineProps<{
13
15
  campaignId: number;
@@ -17,6 +19,13 @@ const state = usePlaytestBuildAccess(props.campaignId);
17
19
 
18
20
  const hasAnyBuild = computed(() => state.assets.value.length > 0);
19
21
 
22
+ // Available at ANY grant status, including pending_nda/revoked — a tester
23
+ // stuck in the sign flow itself (e.g. never got the OTP email) or whose
24
+ // access was pulled still needs a way to reach the studio privately. Never
25
+ // a public wall.
26
+ const showReportForm = ref(false);
27
+ const reportFormRef = ref<InstanceType<typeof ReportProblemForm> | null>(null);
28
+
20
29
  onMounted(async () => {
21
30
  await state.loadAssets();
22
31
  if (hasAnyBuild.value) {
@@ -28,6 +37,22 @@ async function onDownload(assetId: number) {
28
37
  const url = await state.getDownloadUrl(assetId);
29
38
  if (url) window.open(url, "_blank", "noopener");
30
39
  }
40
+
41
+ function openReportForm() {
42
+ state.reportSuccess.value = false;
43
+ showReportForm.value = true;
44
+ }
45
+
46
+ async function onReportSubmit(payload: StoreBuildProblemReportPayload) {
47
+ // showReportForm stays true on success — the reportSuccess branch inside
48
+ // the v-else block below is what actually renders the confirmation.
49
+ await state.submitProblemReport(payload);
50
+ }
51
+
52
+ async function onUploadReportMedia(file: File) {
53
+ const url = await state.uploadReportMedia(file);
54
+ reportFormRef.value?.setMediaUrl(url);
55
+ }
31
56
  </script>
32
57
 
33
58
  <template>
@@ -57,9 +82,50 @@ async function onDownload(assetId: number) {
57
82
  @download="onDownload"
58
83
  />
59
84
 
60
- <p v-else-if="state.grant.value?.status === 'revoked'" class="build-access-panel__revoked">
61
- {{ $t("playtest.build_access.revoked", "Your access to this build has been revoked by the studio.") }}
62
- </p>
85
+ <div v-else-if="state.grant.value?.status === 'revoked'" class="build-access-panel__revoked">
86
+ <p class="build-access-panel__revoked-title">
87
+ {{ $t("playtest.build_access.revoked", "Your access to this build has been revoked.") }}
88
+ </p>
89
+ <p class="build-access-panel__revoked-hint">
90
+ {{ $t("playtest.build_access.revoked_hint", "This is usually done by the studio running the campaign.") }}
91
+ <button type="button" class="build-access-panel__revoked-link" @click="openReportForm">
92
+ {{ $t("playtest.build_access.revoked_contact_link", "If you think this is a mistake, let them know.") }}
93
+ </button>
94
+ </p>
95
+ </div>
96
+
97
+ <div v-if="state.grant.value" class="build-access-panel__contact">
98
+ <button
99
+ v-if="!showReportForm"
100
+ type="button"
101
+ class="build-access-panel__contact-toggle"
102
+ @click="openReportForm"
103
+ >
104
+ {{ $t("playtest.build_access.report_problem", "Report a problem") }}
105
+ </button>
106
+
107
+ <template v-else>
108
+ <div v-if="state.reportSuccess.value" class="build-access-panel__report-sent-wrap">
109
+ <p class="build-access-panel__report-sent">
110
+ {{ $t("playtest.build_access.report_sent", "Sent — only the studio can see this.") }}
111
+ </p>
112
+ <button type="button" class="build-access-panel__contact-toggle" @click="openReportForm">
113
+ {{ $t("playtest.build_access.report_another", "Send another report") }}
114
+ </button>
115
+ </div>
116
+ <template v-else>
117
+ <ReportProblemForm
118
+ ref="reportFormRef"
119
+ :assets="state.assets.value"
120
+ :submitting="state.reportSubmitting.value"
121
+ :uploading="state.reportUploading.value"
122
+ @submit="onReportSubmit"
123
+ @upload-media="onUploadReportMedia"
124
+ />
125
+ <p v-if="state.reportError.value" class="build-access-panel__report-error">{{ state.reportError.value }}</p>
126
+ </template>
127
+ </template>
128
+ </div>
63
129
  </div>
64
130
  </template>
65
131
 
@@ -83,10 +149,67 @@ async function onDownload(assetId: number) {
83
149
  }
84
150
 
85
151
  &__revoked {
152
+ display: flex;
153
+ flex-direction: column;
154
+ gap: 6px;
155
+ }
156
+
157
+ &__revoked-title {
86
158
  font-size: 13px;
87
159
  color: var(--danger, #e53935);
88
160
  margin: 0;
89
161
  }
162
+
163
+ &__revoked-hint {
164
+ font-size: 12px;
165
+ color: var(--secondary-info-fg, #888);
166
+ margin: 0;
167
+ }
168
+
169
+ &__revoked-link {
170
+ background: none;
171
+ border: none;
172
+ padding: 0;
173
+ margin-left: 4px;
174
+ font-size: 12px;
175
+ color: var(--primary, #d297ff);
176
+ cursor: pointer;
177
+ text-decoration: underline;
178
+ }
179
+
180
+ &__contact {
181
+ margin-top: 14px;
182
+ padding-top: 14px;
183
+ border-top: 1px solid var(--button-secondary-default-bg);
184
+ }
185
+
186
+ &__contact-toggle {
187
+ background: none;
188
+ border: none;
189
+ padding: 0;
190
+ font-size: 12px;
191
+ color: var(--primary, #d297ff);
192
+ cursor: pointer;
193
+ }
194
+
195
+ &__report-sent-wrap {
196
+ display: flex;
197
+ flex-direction: column;
198
+ gap: 8px;
199
+ align-items: flex-start;
200
+ }
201
+
202
+ &__report-sent {
203
+ font-size: 12px;
204
+ color: var(--primary, #d297ff);
205
+ margin: 0;
206
+ }
207
+
208
+ &__report-error {
209
+ font-size: 12px;
210
+ color: var(--danger, #e53935);
211
+ margin: 8px 0 0;
212
+ }
90
213
  }
91
214
 
92
215
  @keyframes build-access-panel-pulse {
@@ -37,11 +37,14 @@ function formatSize(bytes: number | null): string {
37
37
 
38
38
  <div v-else class="build-download__list">
39
39
  <div v-for="asset in assets" :key="asset.id" class="build-download__row">
40
- <div class="build-download__info">
41
- <span class="build-download__platform">{{ asset.platform }}</span>
42
- <span class="build-download__filename">{{ asset.original_filename }}</span>
43
- <span v-if="asset.version_label" class="build-download__version">{{ asset.version_label }}</span>
44
- <span v-if="asset.size_bytes" class="build-download__size">{{ formatSize(asset.size_bytes) }}</span>
40
+ <div class="build-download__main">
41
+ <div class="build-download__info">
42
+ <span class="build-download__platform">{{ asset.platform }}</span>
43
+ <span class="build-download__filename">{{ asset.original_filename }}</span>
44
+ <span v-if="asset.version_label" class="build-download__version">{{ asset.version_label }}</span>
45
+ <span v-if="asset.size_bytes" class="build-download__size">{{ formatSize(asset.size_bytes) }}</span>
46
+ </div>
47
+ <p v-if="asset.notes" class="build-download__notes">{{ asset.notes }}</p>
45
48
  </div>
46
49
  <button
47
50
  type="button"
@@ -96,6 +99,13 @@ function formatSize(bytes: number | null): string {
96
99
  border: 1px solid var(--bg-app-badge);
97
100
  }
98
101
 
102
+ &__main {
103
+ display: flex;
104
+ flex-direction: column;
105
+ gap: 4px;
106
+ flex: 1;
107
+ }
108
+
99
109
  &__info {
100
110
  display: flex;
101
111
  flex-wrap: wrap;
@@ -104,6 +114,13 @@ function formatSize(bytes: number | null): string {
104
114
  font-size: 13px;
105
115
  }
106
116
 
117
+ &__notes {
118
+ white-space: pre-wrap;
119
+ font-size: 11px;
120
+ color: var(--secondary-info-fg, #888);
121
+ margin: 0;
122
+ }
123
+
107
124
  &__platform {
108
125
  text-transform: uppercase;
109
126
  font-size: 10px;
@@ -0,0 +1,143 @@
1
+ <script setup lang="ts">
2
+ // Private tester -> studio message about a build-access grant — never a
3
+ // public wall. Structural mirror of playtest/lqa/FileIssueForm.vue, minus
4
+ // the LQA-specific issue_type/severity taxonomy and with evidence made
5
+ // OPTIONAL (a question doesn't need a screenshot, unlike a defect report).
6
+ import { ref, computed } from "vue";
7
+ import type { BuildProblemReportCategory, StoreBuildProblemReportPayload, PlaytestBuildAsset } from "../../../services/playtestTesterService";
8
+
9
+ const props = defineProps<{
10
+ assets: PlaytestBuildAsset[];
11
+ submitting?: boolean;
12
+ uploading?: boolean;
13
+ }>();
14
+
15
+ const emit = defineEmits<{
16
+ (e: "submit", payload: StoreBuildProblemReportPayload): void
17
+ (e: "upload-media", file: File): void
18
+ }>();
19
+
20
+ const CATEGORIES: BuildProblemReportCategory[] = ["question", "installation_problem", "access_problem", "other"];
21
+
22
+ const category = ref<BuildProblemReportCategory | "">("");
23
+ const assetId = ref<number | "">("");
24
+ const message = ref("");
25
+ const mediaUrl = ref<string | null>(null);
26
+
27
+ const canSubmit = computed(() =>
28
+ !!category.value &&
29
+ message.value.trim().length > 0 &&
30
+ !props.submitting
31
+ );
32
+
33
+ function onFileChange(e: Event) {
34
+ const file = (e.target as HTMLInputElement).files?.[0];
35
+ if (file) emit("upload-media", file);
36
+ }
37
+
38
+ function setMediaUrl(url: string | null) {
39
+ mediaUrl.value = url;
40
+ }
41
+
42
+ defineExpose({ setMediaUrl });
43
+
44
+ function submit() {
45
+ if (!canSubmit.value || !category.value) return;
46
+ emit("submit", {
47
+ category: category.value,
48
+ message: message.value.trim(),
49
+ playtest_build_asset_id: assetId.value ? Number(assetId.value) : undefined,
50
+ media_url: mediaUrl.value ?? undefined,
51
+ });
52
+
53
+ category.value = "";
54
+ assetId.value = "";
55
+ message.value = "";
56
+ mediaUrl.value = null;
57
+ }
58
+ </script>
59
+
60
+ <template>
61
+ <div class="report-problem-form">
62
+ <div class="report-problem-form__row">
63
+ <label class="report-problem-form__label">{{ $t("playtest.build_access.report_category", "What's this about?") }}</label>
64
+ <select v-model="category" class="report-problem-form__select">
65
+ <option value="" disabled>{{ $t("playtest.build_access.select_category_placeholder", "Select…") }}</option>
66
+ <option v-for="c in CATEGORIES" :key="c" :value="c">
67
+ {{ $t(`playtest.build_access.report_category_${c}`) }}
68
+ </option>
69
+ </select>
70
+ </div>
71
+
72
+ <div v-if="assets.length" class="report-problem-form__row">
73
+ <label class="report-problem-form__label">{{ $t("playtest.build_access.report_related_build", "Related build (optional)") }}</label>
74
+ <select v-model="assetId" class="report-problem-form__select">
75
+ <option value="">{{ $t("playtest.build_access.report_select_build_placeholder", "General question / not build-specific") }}</option>
76
+ <option v-for="asset in assets" :key="asset.id" :value="asset.id">
77
+ {{ asset.platform }} — {{ asset.original_filename }}
78
+ </option>
79
+ </select>
80
+ </div>
81
+
82
+ <div class="report-problem-form__row">
83
+ <label class="report-problem-form__label">{{ $t("playtest.build_access.report_message", "Message (required)") }}</label>
84
+ <textarea v-model="message" rows="3" class="report-problem-form__textarea" />
85
+ </div>
86
+
87
+ <div class="report-problem-form__row">
88
+ <label class="report-problem-form__label">{{ $t("playtest.build_access.report_attach_screenshot", "Attach a screenshot (optional)") }}</label>
89
+ <input type="file" accept="image/*" @change="onFileChange">
90
+ <span v-if="uploading" class="report-problem-form__uploading">{{ $t("playtest.lqa.uploading", "Uploading…") }}</span>
91
+ <span v-else-if="mediaUrl" class="report-problem-form__uploaded">{{ $t("playtest.lqa.uploaded", "Attached") }}</span>
92
+ </div>
93
+
94
+ <button type="button" class="btn primary report-problem-form__submit" :disabled="!canSubmit" @click="submit">
95
+ {{ submitting ? $t("playtest.build_access.sending_report", "Sending…") : $t("playtest.build_access.report_submit", "Send to studio") }}
96
+ </button>
97
+ </div>
98
+ </template>
99
+
100
+ <style scoped lang="scss">
101
+ .report-problem-form {
102
+ &__row {
103
+ margin-bottom: 14px;
104
+ }
105
+
106
+ &__label {
107
+ display: block;
108
+ font-size: 12px;
109
+ color: var(--secondary-info-fg, #aaa);
110
+ margin-bottom: 6px;
111
+ }
112
+
113
+ &__select,
114
+ &__textarea {
115
+ width: 100%;
116
+ background: var(--bg-app-badge, #1a1a1a);
117
+ border: 1px solid var(--button-secondary-default-bg, #2a2a2a);
118
+ color: var(--text-primary, #fff);
119
+ padding: 10px;
120
+ font-size: 13px;
121
+ }
122
+
123
+ &__textarea {
124
+ resize: vertical;
125
+ }
126
+
127
+ &__uploading,
128
+ &__uploaded {
129
+ display: inline-block;
130
+ margin-left: 8px;
131
+ font-size: 11px;
132
+ color: var(--secondary-info-fg, #888);
133
+ }
134
+
135
+ &__uploaded {
136
+ color: var(--primary, #D297FF);
137
+ }
138
+
139
+ &__submit {
140
+ margin-top: 4px;
141
+ }
142
+ }
143
+ </style>
@@ -4,8 +4,11 @@ import {
4
4
  initiateBuildNda,
5
5
  fetchBuildAssets,
6
6
  fetchBuildDownloadUrl,
7
+ storeBuildProblemReport,
8
+ uploadPlaytestMedia,
7
9
  type PlaytestBuildGrant,
8
10
  type PlaytestBuildAsset,
11
+ type StoreBuildProblemReportPayload,
9
12
  } from "../services/playtestTesterService";
10
13
  import {
11
14
  fetchSigningSession,
@@ -194,6 +197,41 @@ export function usePlaytestBuildAccess(campaignId: number) {
194
197
  }
195
198
  }
196
199
 
200
+ // ── Problem reports (private, any grant status) ──────────────────────
201
+ const reportSubmitting = ref(false);
202
+ const reportUploading = ref(false);
203
+ const reportSuccess = ref(false);
204
+ const reportError = ref<string | null>(null);
205
+
206
+ /** Never gated on grant status — a tester stuck before NDA sign still needs to reach the studio. */
207
+ async function submitProblemReport(payload: StoreBuildProblemReportPayload) {
208
+ if (!grant.value) return false;
209
+ reportError.value = null;
210
+ reportSubmitting.value = true;
211
+ try {
212
+ await storeBuildProblemReport(grant.value.id, payload);
213
+ reportSuccess.value = true;
214
+ return true;
215
+ } catch (e: any) {
216
+ reportError.value = errorMessage(e, "Could not send your report. Please try again.");
217
+ return false;
218
+ } finally {
219
+ reportSubmitting.value = false;
220
+ }
221
+ }
222
+
223
+ async function uploadReportMedia(file: File): Promise<string | null> {
224
+ reportUploading.value = true;
225
+ try {
226
+ const res = await uploadPlaytestMedia(file);
227
+ return (res.data as any)?.data?.url ?? null;
228
+ } catch {
229
+ return null;
230
+ } finally {
231
+ reportUploading.value = false;
232
+ }
233
+ }
234
+
197
235
  return {
198
236
  loading,
199
237
  submitting,
@@ -213,6 +251,12 @@ export function usePlaytestBuildAccess(campaignId: number) {
213
251
  sign,
214
252
  getDownloadUrl,
215
253
  resetErrors,
254
+ reportSubmitting,
255
+ reportUploading,
256
+ reportSuccess,
257
+ reportError,
258
+ submitProblemReport,
259
+ uploadReportMedia,
216
260
  };
217
261
  }
218
262
 
package/locales/de.json CHANGED
@@ -24,10 +24,27 @@
24
24
  "enter_code": "Gib den 6-stelligen Code ein, den wir dir per E-Mail geschickt haben",
25
25
  "nda_title": "Unterschreibe die NDA, um die Build freizuschalten",
26
26
  "no_builds": "Noch keine Builds verfügbar",
27
+ "report_another": "Weitere Meldung senden",
28
+ "report_attach_screenshot": "Screenshot anhängen (optional)",
29
+ "report_category": "Worum geht es?",
30
+ "report_category_access_problem": "Zugriffs-/Download-Problem",
31
+ "report_category_installation_problem": "Installationsproblem",
32
+ "report_category_other": "Sonstiges",
33
+ "report_category_question": "Frage",
34
+ "report_message": "Nachricht (erforderlich)",
35
+ "report_problem": "Problem melden",
36
+ "report_related_build": "Zugehörige Build (optional)",
37
+ "report_select_build_placeholder": "Allgemeine Frage / nicht build-spezifisch",
38
+ "report_sent": "Gesendet — nur das Studio sieht das.",
39
+ "report_submit": "An Studio senden",
27
40
  "resend_code": "Code erneut senden",
28
- "revoked": "Dein Build-Zugang wurde widerrufen",
41
+ "revoked": "Dein Zugriff auf diese Build wurde widerrufen.",
42
+ "revoked_contact_link": "Falls du denkst, das ist ein Irrtum, lass es das Studio wissen.",
43
+ "revoked_hint": "Das wird normalerweise vom Studio veranlasst, das die Kampagne betreibt.",
44
+ "select_category_placeholder": "Auswählen…",
29
45
  "send_code": "Bestätigungscode senden",
30
46
  "sending_code": "Code wird gesendet…",
47
+ "sending_report": "Wird gesendet…",
31
48
  "sign": "Unterschreiben",
32
49
  "signing": "Wird unterschrieben…",
33
50
  "verify": "Bestätigen",
@@ -372,6 +389,7 @@
372
389
  "already_requested": "Sie haben bereits eine Anfrage für diese Kampagne gestellt.",
373
390
  "sold_out": "Alle Keys für diese Kampagne wurden bereits eingelöst.",
374
391
  "options_unavailable": "Diese Kampagne hat noch Keys, aber es ist noch keine Plattform oder Region dafür eingerichtet, daher können keine Anfragen gesendet werden. Das Studio muss die Einrichtung abschließen — auf deiner Seite ist alles in Ordnung.",
392
+ "countries_only": "Nur in {countries}",
375
393
  "keys_left": "{n} Key übrig | {n} Keys übrig",
376
394
  "keys_low_stock": "Letzter Key | Letzte {n}",
377
395
  "back_to_campaign": "Zur Kampagne zurückkehren",
package/locales/en.json CHANGED
@@ -24,10 +24,27 @@
24
24
  "enter_code": "Enter the 6-digit code we emailed you",
25
25
  "nda_title": "Sign the NDA to unlock the build",
26
26
  "no_builds": "No builds available yet",
27
+ "report_another": "Send another report",
28
+ "report_attach_screenshot": "Attach a screenshot (optional)",
29
+ "report_category": "What's this about?",
30
+ "report_category_access_problem": "Access / download problem",
31
+ "report_category_installation_problem": "Installation problem",
32
+ "report_category_other": "Other",
33
+ "report_category_question": "Question",
34
+ "report_message": "Message (required)",
35
+ "report_problem": "Report a problem",
36
+ "report_related_build": "Related build (optional)",
37
+ "report_select_build_placeholder": "General question / not build-specific",
38
+ "report_sent": "Sent — only the studio can see this.",
39
+ "report_submit": "Send to studio",
27
40
  "resend_code": "Resend code",
28
- "revoked": "Your build access has been revoked",
41
+ "revoked": "Your access to this build has been revoked.",
42
+ "revoked_contact_link": "If you think this is a mistake, let them know.",
43
+ "revoked_hint": "This is usually done by the studio running the campaign.",
44
+ "select_category_placeholder": "Select…",
29
45
  "send_code": "Send verification code",
30
46
  "sending_code": "Sending code…",
47
+ "sending_report": "Sending…",
31
48
  "sign": "Sign",
32
49
  "signing": "Signing…",
33
50
  "verify": "Verify",
@@ -372,6 +389,7 @@
372
389
  "already_requested": "You have already submitted a request for this campaign.",
373
390
  "sold_out": "All keys for this campaign have already been claimed.",
374
391
  "options_unavailable": "This campaign still has keys, but no platform or region is set up for them yet, so requests can't be sent. The studio needs to finish setting it up — nothing on your side is wrong.",
392
+ "countries_only": "Only in {countries}",
375
393
  "keys_left": "{n} key left | {n} keys left",
376
394
  "keys_low_stock": "Last key | Last {n}",
377
395
  "back_to_campaign": "Back to campaign",
package/locales/es.json CHANGED
@@ -24,10 +24,27 @@
24
24
  "enter_code": "Ingresa el código de 6 dígitos que te enviamos por correo",
25
25
  "nda_title": "Firma el NDA para desbloquear la build",
26
26
  "no_builds": "Todavía no hay builds disponibles",
27
+ "report_another": "Enviar otro reporte",
28
+ "report_attach_screenshot": "Adjuntar una captura de pantalla (opcional)",
29
+ "report_category": "¿De qué se trata?",
30
+ "report_category_access_problem": "Problema de acceso / descarga",
31
+ "report_category_installation_problem": "Problema de instalación",
32
+ "report_category_other": "Otro",
33
+ "report_category_question": "Pregunta",
34
+ "report_message": "Mensaje (obligatorio)",
35
+ "report_problem": "Reportar un problema",
36
+ "report_related_build": "Build relacionada (opcional)",
37
+ "report_select_build_placeholder": "Pregunta general / no específica de una build",
38
+ "report_sent": "Enviado — solo el estudio puede verlo.",
39
+ "report_submit": "Enviar al estudio",
27
40
  "resend_code": "Reenviar código",
28
- "revoked": "Tu acceso a la build ha sido revocado",
41
+ "revoked": "Tu acceso a esta build ha sido revocado.",
42
+ "revoked_contact_link": "Si crees que esto es un error, avísale al estudio.",
43
+ "revoked_hint": "Esto normalmente lo hace el estudio a cargo de la campaña.",
44
+ "select_category_placeholder": "Selecciona…",
29
45
  "send_code": "Enviar código de verificación",
30
46
  "sending_code": "Enviando código…",
47
+ "sending_report": "Enviando…",
31
48
  "sign": "Firmar",
32
49
  "signing": "Firmando…",
33
50
  "verify": "Verificar",
@@ -372,6 +389,7 @@
372
389
  "already_requested": "Ya has enviado una solicitud para esta campaña.",
373
390
  "sold_out": "Todas las claves de esta campaña ya fueron reclamadas.",
374
391
  "options_unavailable": "Esta campaña todavía tiene claves, pero aún no hay ninguna plataforma o región configurada para ellas, así que no se pueden enviar solicitudes. El estudio debe terminar la configuración — no hay nada mal de tu lado.",
392
+ "countries_only": "Solo en {countries}",
375
393
  "keys_left": "{n} clave restante | {n} claves restantes",
376
394
  "keys_low_stock": "Última clave | Últimas {n}",
377
395
  "back_to_campaign": "Volver a la campaña",
@@ -24,10 +24,27 @@
24
24
  "enter_code": "Digite o código de 6 dígitos que enviamos por e-mail",
25
25
  "nda_title": "Assine o NDA para desbloquear a build",
26
26
  "no_builds": "Nenhuma build disponível ainda",
27
+ "report_another": "Enviar outro relato",
28
+ "report_attach_screenshot": "Anexar uma captura de tela (opcional)",
29
+ "report_category": "Sobre o que é isso?",
30
+ "report_category_access_problem": "Problema de acesso / download",
31
+ "report_category_installation_problem": "Problema de instalação",
32
+ "report_category_other": "Outro",
33
+ "report_category_question": "Dúvida",
34
+ "report_message": "Mensagem (obrigatória)",
35
+ "report_problem": "Relatar um problema",
36
+ "report_related_build": "Build relacionada (opcional)",
37
+ "report_select_build_placeholder": "Dúvida geral / não específica de uma build",
38
+ "report_sent": "Enviado — só o estúdio vê isso.",
39
+ "report_submit": "Enviar para o estúdio",
27
40
  "resend_code": "Reenviar código",
28
- "revoked": "Seu acesso à build foi revogado",
41
+ "revoked": "Seu acesso a esta build foi revogado.",
42
+ "revoked_contact_link": "Se você acha que isso é um engano, avise o estúdio.",
43
+ "revoked_hint": "Isso geralmente é feito pelo estúdio responsável pela campanha.",
44
+ "select_category_placeholder": "Selecione…",
29
45
  "send_code": "Enviar código de verificação",
30
46
  "sending_code": "Enviando código…",
47
+ "sending_report": "Enviando…",
31
48
  "sign": "Assinar",
32
49
  "signing": "Assinando…",
33
50
  "verify": "Verificar",
@@ -372,6 +389,7 @@
372
389
  "already_requested": "Você já enviou uma solicitação para esta campanha.",
373
390
  "sold_out": "Todas as chaves desta campanha já foram resgatadas.",
374
391
  "options_unavailable": "Esta campanha ainda tem chaves, mas nenhuma plataforma ou região foi configurada para elas, então não dá para enviar pedidos. O estúdio precisa concluir a configuração — não há nada errado do seu lado.",
392
+ "countries_only": "Somente em {countries}",
375
393
  "keys_left": "{n} chave restante | {n} chaves restantes",
376
394
  "keys_low_stock": "Última chave | Últimas {n}",
377
395
  "back_to_campaign": "Voltar para a campanha",
package/locales/ro.json CHANGED
@@ -24,10 +24,27 @@
24
24
  "enter_code": "Introdu codul din 6 cifre pe care ți l-am trimis pe e-mail",
25
25
  "nda_title": "Semnează NDA-ul pentru a debloca build-ul",
26
26
  "no_builds": "Niciun build disponibil momentan",
27
+ "report_another": "Trimite un alt raport",
28
+ "report_attach_screenshot": "Atașează o captură de ecran (opțional)",
29
+ "report_category": "Despre ce este vorba?",
30
+ "report_category_access_problem": "Problemă de acces / descărcare",
31
+ "report_category_installation_problem": "Problemă de instalare",
32
+ "report_category_other": "Altceva",
33
+ "report_category_question": "Întrebare",
34
+ "report_message": "Mesaj (obligatoriu)",
35
+ "report_problem": "Raportează o problemă",
36
+ "report_related_build": "Build asociat (opțional)",
37
+ "report_select_build_placeholder": "Întrebare generală / fără legătură cu un build anume",
38
+ "report_sent": "Trimis — doar studioul vede asta.",
39
+ "report_submit": "Trimite studioului",
27
40
  "resend_code": "Retrimite codul",
28
- "revoked": "Accesul tău la build a fost revocat",
41
+ "revoked": "Accesul tău la acest build a fost revocat.",
42
+ "revoked_contact_link": "Dacă crezi că e o greșeală, anunță studioul.",
43
+ "revoked_hint": "Acest lucru este de obicei făcut de studioul care rulează campania.",
44
+ "select_category_placeholder": "Selectează…",
29
45
  "send_code": "Trimite codul de verificare",
30
46
  "sending_code": "Se trimite codul…",
47
+ "sending_report": "Se trimite…",
31
48
  "sign": "Semnează",
32
49
  "signing": "Se semnează…",
33
50
  "verify": "Verifică",
@@ -372,6 +389,7 @@
372
389
  "already_requested": "Ați trimis deja o cerere pentru această campanie.",
373
390
  "sold_out": "Toate cheile pentru această campanie au fost deja revendicate.",
374
391
  "options_unavailable": "Această campanie mai are chei, dar încă nu este configurată nicio platformă sau regiune pentru ele, așa că nu se pot trimite cereri. Studioul trebuie să finalizeze configurarea — nu este nimic greșit din partea ta.",
392
+ "countries_only": "Doar în {countries}",
375
393
  "keys_left": "{n} cheie rămasă | {n} chei rămase",
376
394
  "keys_low_stock": "Ultima cheie | Ultimele {n}",
377
395
  "back_to_campaign": "Înapoi la campanie",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.16.23",
3
+ "version": "1.16.25",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -230,13 +230,22 @@
230
230
  {{ card.type }}
231
231
  </span>
232
232
  </div>
233
- <div v-if="card.platforms && card.platforms.length" class="card-platforms">
233
+ <div v-if="(card.platforms && card.platforms.length) || (card.stores && card.stores.length)" class="card-platforms">
234
234
  <span v-for="p in card.platforms.slice(0, 4)" :key="p" class="card-platform-tag">
235
235
  {{ platformAbbr(p) }}
236
236
  </span>
237
237
  <span v-if="card.platforms.length > 4" class="card-platform-tag card-platform-more">
238
238
  +{{ card.platforms.length - 4 }}
239
239
  </span>
240
+ <span v-for="st in card.stores.slice(0, 3)" :key="st" class="card-platform-tag card-store-tag">
241
+ {{ st }}
242
+ </span>
243
+ </div>
244
+ <!-- Só aparece quando o estúdio restringiu de verdade: a
245
+ lista vazia significa aberta a todo mundo, e mostrar
246
+ isso em toda campanha viraria ruído. -->
247
+ <div v-if="card.countries && card.countries.length" class="card-countries">
248
+ {{ $t("keys.campaigns.countries_only", { countries: card.countries.slice(0, 3).join(", ") }) }}
240
249
  </div>
241
250
 
242
251
  <div class="buttons">
@@ -695,6 +704,13 @@ async function loadCampaigns(page = 1) {
695
704
  genres: (item.game?.genres || []).map((g: any) => g.name).filter(Boolean).slice(0, 2),
696
705
  studioName: item.company?.name || null,
697
706
  regions: (item.regions || []).map((r: any) => r.name).filter(Boolean),
707
+ // Onde o código é resgatado. Plataforma diz onde o jogo roda;
708
+ // "PC (Microsoft Windows)" não diz se a chave entra no Steam
709
+ // ou na Epic, que é a primeira coisa de que quem pede precisa.
710
+ stores: (item.stores || []).map((st: any) => st.name).filter(Boolean),
711
+ // Vazio = aberta a todo mundo, que é o caso da maioria. Só
712
+ // vira informação quando o estúdio restringiu de verdade.
713
+ countries: (item.countries || []).map((c: any) => c.name).filter(Boolean),
698
714
  userRequestSlug: null,
699
715
  }
700
716
  })
@@ -1608,6 +1624,20 @@ onUnmounted(() => {
1608
1624
  border-style: dashed;
1609
1625
  opacity: 0.8;
1610
1626
  }
1627
+ // A loja é a informação mais acionável das duas: o
1628
+ // destinatário precisa dela pra saber onde colar o
1629
+ // código. Preenchida em vez de contornada pra
1630
+ // separar visualmente da plataforma ao lado.
1631
+ .card-store-tag {
1632
+ background: var(--secondary-info-fg);
1633
+ color: var(--primary-bg);
1634
+ text-transform: none;
1635
+ }
1636
+ }
1637
+ .card-countries {
1638
+ font-size: 11px;
1639
+ color: var(--secondary-info-fg);
1640
+ margin-top: 4px;
1611
1641
  }
1612
1642
 
1613
1643
  .buttons {
@@ -530,6 +530,7 @@ export interface PlaytestBuildAsset {
530
530
  original_filename: string
531
531
  mime_type: string | null
532
532
  size_bytes: number | null
533
+ notes: string | null
533
534
  status: 'pending' | 'ready' | 'failed'
534
535
  }
535
536
 
@@ -556,3 +557,40 @@ export const fetchBuildDownloadUrl = (grantId: number, assetId: number) =>
556
557
  httpService.get<{ data: { url: string; expires_at: string } }>(
557
558
  `/playtest/build-access/${grantId}/builds/${assetId}/download`,
558
559
  )
560
+
561
+ // ---------------------------------------------------------------------------
562
+ // Build problem reports — a private, one-way tester -> studio channel, never
563
+ // a public wall (this whole feature exists to keep an unreleased build
564
+ // confidential). Works at ANY grant status, including pending_nda — a
565
+ // tester stuck in the sign flow itself (e.g. never got the OTP email) still
566
+ // needs a way to reach the studio.
567
+ // ---------------------------------------------------------------------------
568
+
569
+ export type BuildProblemReportCategory = 'question' | 'installation_problem' | 'access_problem' | 'other'
570
+
571
+ export interface StoreBuildProblemReportPayload {
572
+ category: BuildProblemReportCategory
573
+ message: string
574
+ playtest_build_asset_id?: number
575
+ media_url?: string
576
+ }
577
+
578
+ export interface PlaytestBuildProblemReport {
579
+ id: number
580
+ playtest_campaign_id: number
581
+ playtest_build_grant_id: number
582
+ playtest_build_asset_id: number | null
583
+ category: BuildProblemReportCategory
584
+ message: string
585
+ media_url: string | null
586
+ status: 'open' | 'in_progress' | 'resolved'
587
+ studio_response: string | null
588
+ responded_at: string | null
589
+ created_at: string
590
+ }
591
+
592
+ export const storeBuildProblemReport = (grantId: number, payload: StoreBuildProblemReportPayload) =>
593
+ httpService.post<{ data: PlaytestBuildProblemReport }>(
594
+ `/playtest/build-access/${grantId}/problem-reports`,
595
+ payload,
596
+ )