@mundogamernetwork/shared-ui 1.16.24 → 1.16.26
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/playtest/PlaytestResponseFlow.vue +55 -9
- package/composables/usePlaytestResponse.ts +14 -3
- package/locales/de.json +4 -1
- package/locales/en.json +4 -1
- package/locales/es.json +4 -1
- package/locales/pt-BR.json +4 -1
- package/locales/ro.json +4 -1
- package/package.json +1 -1
- package/pages/key-campaigns/index.vue +31 -1
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// loadAccessibilityCriteria) are cheap and idempotent to re-run client-side.
|
|
22
22
|
import { ref, computed, onMounted, watch } from "vue";
|
|
23
23
|
import { usePlaytestResponse } from "../../composables/usePlaytestResponse";
|
|
24
|
+
import type { PlaytestCampaign } from "../../services/playtestTesterService";
|
|
24
25
|
|
|
25
26
|
import PlaytestBrief from "./PlaytestBrief.vue";
|
|
26
27
|
import PlaytestPendingConfirmation from "./PlaytestPendingConfirmation.vue";
|
|
@@ -54,6 +55,19 @@ const props = defineProps<{
|
|
|
54
55
|
// moderated: explicit session id when resuming/joining a specific
|
|
55
56
|
// already-booked session (skips the slot-browse step)
|
|
56
57
|
sessionId?: number;
|
|
58
|
+
// Studio "preview as tester" mode (agency-frontend's campaign hub Preview
|
|
59
|
+
// tab): a campaign owner can never appear in available() (it excludes
|
|
60
|
+
// their own campaigns) and would never pass real eligibility rules
|
|
61
|
+
// (certifications they don't hold) anyway. eligibilityFetcher supplies a
|
|
62
|
+
// synthetic eligible:true snapshot from the owner-only preview endpoint
|
|
63
|
+
// instead of the tester-only available() list. previewMode disables every
|
|
64
|
+
// interactive control so the owner sees exactly what a tester would see
|
|
65
|
+
// without being able to record a response — though every submit endpoint
|
|
66
|
+
// already independently rejects the campaign owner server-side
|
|
67
|
+
// regardless (see PlaytestService::submitResponse()), so this is UX
|
|
68
|
+
// polish on top of a real security boundary, not the boundary itself.
|
|
69
|
+
eligibilityFetcher?: () => Promise<PlaytestCampaign | null>;
|
|
70
|
+
previewMode?: boolean;
|
|
57
71
|
}>();
|
|
58
72
|
|
|
59
73
|
const emit = defineEmits<{
|
|
@@ -61,7 +75,7 @@ const emit = defineEmits<{
|
|
|
61
75
|
(e: "browse-more"): void
|
|
62
76
|
}>();
|
|
63
77
|
|
|
64
|
-
const flow = usePlaytestResponse(props.campaignId);
|
|
78
|
+
const flow = usePlaytestResponse(props.campaignId, { eligibilityFetcher: props.eligibilityFetcher });
|
|
65
79
|
|
|
66
80
|
const done = ref(false);
|
|
67
81
|
|
|
@@ -103,6 +117,7 @@ function browseMore() {
|
|
|
103
117
|
const enabledSectionKeys = computed(() => flow.campaign.value?.enabled_sections ?? []);
|
|
104
118
|
|
|
105
119
|
async function submitBaselineFlow() {
|
|
120
|
+
if (props.previewMode) return;
|
|
106
121
|
const result = await flow.submitBaseline();
|
|
107
122
|
if (result) handleDone();
|
|
108
123
|
}
|
|
@@ -123,7 +138,7 @@ const conceptPollComponent = computed(() => {
|
|
|
123
138
|
});
|
|
124
139
|
|
|
125
140
|
async function submitConceptPollFlow(payload: { answer: any; reason: string }) {
|
|
126
|
-
if (!props.pollId) return;
|
|
141
|
+
if (props.previewMode || !props.pollId) return;
|
|
127
142
|
const result = await flow.submitConceptPollVote(props.pollId, payload.answer, payload.reason);
|
|
128
143
|
if (result) handleDone();
|
|
129
144
|
}
|
|
@@ -133,6 +148,7 @@ async function submitConceptPollFlow(payload: { answer: any; reason: string }) {
|
|
|
133
148
|
const selectedLocaleId = ref<number | null>(null);
|
|
134
149
|
|
|
135
150
|
async function submitIssueFlow(payload: Parameters<typeof flow.fileLocalizationIssue>[0]) {
|
|
151
|
+
if (props.previewMode) return;
|
|
136
152
|
await flow.fileLocalizationIssue(payload);
|
|
137
153
|
// LQA is a repeatable session (file multiple issues) — no auto "done"
|
|
138
154
|
// transition; the consuming app decides when the tester is finished
|
|
@@ -157,6 +173,9 @@ const allCriteriaComplete = computed(
|
|
|
157
173
|
);
|
|
158
174
|
|
|
159
175
|
async function saveAccessibilityEvaluation(entry: Parameters<typeof flow.setAccessibilityEvaluation>[0]) {
|
|
176
|
+
// CriterionRow auto-saves per row with no busy/disabled prop to gate —
|
|
177
|
+
// this guard is the only stop for it in preview mode.
|
|
178
|
+
if (props.previewMode) return;
|
|
160
179
|
flow.setAccessibilityEvaluation(entry);
|
|
161
180
|
await flow.submitAccessibilityBatch([entry]);
|
|
162
181
|
}
|
|
@@ -187,7 +206,7 @@ const complianceComplete = computed(() => {
|
|
|
187
206
|
});
|
|
188
207
|
|
|
189
208
|
async function submitComplianceFlow() {
|
|
190
|
-
if (!activeChecklist.value) return;
|
|
209
|
+
if (props.previewMode || !activeChecklist.value) return;
|
|
191
210
|
const result = await flow.submitComplianceChecklist(activeChecklist.value.id);
|
|
192
211
|
if (result) handleDone();
|
|
193
212
|
}
|
|
@@ -195,15 +214,18 @@ async function submitComplianceFlow() {
|
|
|
195
214
|
// ── Moderated ─────────────────────────────────────────────────────────────
|
|
196
215
|
|
|
197
216
|
async function claimSlotFlow(slotId: number) {
|
|
217
|
+
if (props.previewMode) return;
|
|
198
218
|
const result = await flow.claimSlot(slotId);
|
|
199
219
|
if (result) handleDone();
|
|
200
220
|
}
|
|
201
221
|
|
|
202
222
|
async function confirmSessionFlow() {
|
|
223
|
+
if (props.previewMode) return;
|
|
203
224
|
if (flow.currentSession.value) await flow.confirmSession(flow.currentSession.value.id);
|
|
204
225
|
}
|
|
205
226
|
|
|
206
227
|
async function cancelSessionFlow() {
|
|
228
|
+
if (props.previewMode) return;
|
|
207
229
|
if (flow.currentSession.value) await flow.cancelSession(flow.currentSession.value.id);
|
|
208
230
|
}
|
|
209
231
|
</script>
|
|
@@ -218,6 +240,10 @@ async function cancelSessionFlow() {
|
|
|
218
240
|
/>
|
|
219
241
|
|
|
220
242
|
<template v-else>
|
|
243
|
+
<p v-if="previewMode" class="playtest-response-flow__preview-banner">
|
|
244
|
+
{{ $t("playtest.flow.preview_banner", "Preview only — this is exactly what a tester sees. Nothing here can be submitted.") }}
|
|
245
|
+
</p>
|
|
246
|
+
|
|
221
247
|
<PlaytestBrief :campaign="flow.campaign.value" :loading="flow.loading.value">
|
|
222
248
|
<p v-if="flow.genericError.value" class="playtest-response-flow__error">
|
|
223
249
|
{{ flow.genericError.value }}
|
|
@@ -236,7 +262,7 @@ async function cancelSessionFlow() {
|
|
|
236
262
|
<button
|
|
237
263
|
type="button"
|
|
238
264
|
class="btn primary playtest-response-flow__submit"
|
|
239
|
-
:disabled="flow.submitting.value"
|
|
265
|
+
:disabled="previewMode || flow.submitting.value"
|
|
240
266
|
@click="submitBaselineFlow"
|
|
241
267
|
>
|
|
242
268
|
{{ $t("playtest.flow.submit_response", "Submit response") }}
|
|
@@ -253,7 +279,7 @@ async function cancelSessionFlow() {
|
|
|
253
279
|
:is="conceptPollComponent"
|
|
254
280
|
v-if="conceptPollComponent"
|
|
255
281
|
:options="flow.poll.value.options"
|
|
256
|
-
:submitting="flow.submitting.value"
|
|
282
|
+
:submitting="previewMode || flow.submitting.value"
|
|
257
283
|
@submit="submitConceptPollFlow"
|
|
258
284
|
/>
|
|
259
285
|
</div>
|
|
@@ -263,7 +289,7 @@ async function cancelSessionFlow() {
|
|
|
263
289
|
<LocaleSelector :locales="flow.locales.value" v-model="selectedLocaleId" />
|
|
264
290
|
<FileIssueForm
|
|
265
291
|
:campaign-locale-id="selectedLocaleId"
|
|
266
|
-
:submitting="flow.submitting.value"
|
|
292
|
+
:submitting="previewMode || flow.submitting.value"
|
|
267
293
|
:uploading="flow.uploading.value"
|
|
268
294
|
@submit="submitIssueFlow"
|
|
269
295
|
@upload-media="uploadLqaMedia"
|
|
@@ -300,7 +326,7 @@ async function cancelSessionFlow() {
|
|
|
300
326
|
<button
|
|
301
327
|
type="button"
|
|
302
328
|
class="btn primary playtest-response-flow__submit"
|
|
303
|
-
:disabled="!complianceComplete || flow.submitting.value"
|
|
329
|
+
:disabled="previewMode || !complianceComplete || flow.submitting.value"
|
|
304
330
|
@click="submitComplianceFlow"
|
|
305
331
|
>
|
|
306
332
|
{{ $t("playtest.flow.submit_checklist", "Submit checklist") }}
|
|
@@ -308,6 +334,9 @@ async function cancelSessionFlow() {
|
|
|
308
334
|
</div>
|
|
309
335
|
|
|
310
336
|
<!-- ── Video Playtest ── -->
|
|
337
|
+
<div v-else-if="category === 'video_playtest' && previewMode" class="playtest-response-flow__video-preview-note">
|
|
338
|
+
{{ $t("playtest.flow.video_preview_unavailable", "Testers record their screen here while playing. Camera/mic capture is not available in preview.") }}
|
|
339
|
+
</div>
|
|
311
340
|
<div v-else-if="category === 'video_playtest'">
|
|
312
341
|
<VideoPlaytestCapture :campaign-id="campaignId" @submitted="handleDone" />
|
|
313
342
|
</div>
|
|
@@ -317,7 +346,7 @@ async function cancelSessionFlow() {
|
|
|
317
346
|
<JoinCallPanel
|
|
318
347
|
v-if="sessionId && flow.currentSession.value"
|
|
319
348
|
:session="flow.currentSession.value"
|
|
320
|
-
:acting="flow.submitting.value"
|
|
349
|
+
:acting="previewMode || flow.submitting.value"
|
|
321
350
|
@confirm="confirmSessionFlow"
|
|
322
351
|
@cancel="cancelSessionFlow"
|
|
323
352
|
/>
|
|
@@ -327,7 +356,7 @@ async function cancelSessionFlow() {
|
|
|
327
356
|
:key="slot.id"
|
|
328
357
|
variant="slot"
|
|
329
358
|
:slot="slot"
|
|
330
|
-
:claiming="flow.submitting.value"
|
|
359
|
+
:claiming="previewMode || flow.submitting.value"
|
|
331
360
|
@claim="claimSlotFlow"
|
|
332
361
|
/>
|
|
333
362
|
</div>
|
|
@@ -373,5 +402,22 @@ async function cancelSessionFlow() {
|
|
|
373
402
|
flex-direction: column;
|
|
374
403
|
gap: 10px;
|
|
375
404
|
}
|
|
405
|
+
|
|
406
|
+
&__preview-banner {
|
|
407
|
+
background: rgba(253, 178, 21, 0.1);
|
|
408
|
+
border: 1px solid rgba(253, 178, 21, 0.3);
|
|
409
|
+
color: var(--card-article-title, #fff);
|
|
410
|
+
font-size: 13px;
|
|
411
|
+
font-weight: 600;
|
|
412
|
+
padding: 10px 14px;
|
|
413
|
+
margin-bottom: 16px;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
&__video-preview-note {
|
|
417
|
+
color: var(--secondary-info-fg, #aaa);
|
|
418
|
+
font-size: 13px;
|
|
419
|
+
padding: 16px;
|
|
420
|
+
border: 1px dashed var(--card-border, #333);
|
|
421
|
+
}
|
|
376
422
|
}
|
|
377
423
|
</style>
|
|
@@ -47,8 +47,18 @@ import {
|
|
|
47
47
|
*
|
|
48
48
|
* One instance per campaign visit — instantiate fresh per mount
|
|
49
49
|
* (PlaytestResponseFlow.vue owns the instance), not as a global singleton.
|
|
50
|
+
*
|
|
51
|
+
* `options.eligibilityFetcher` lets a caller supply the campaign+eligibility
|
|
52
|
+
* snapshot from a different source than the tester-only available() list —
|
|
53
|
+
* used by the studio "preview as tester" mode, which cannot appear in
|
|
54
|
+
* available() (it excludes the caller's own campaigns) and needs a
|
|
55
|
+
* synthetic eligible:true snapshot instead. Omit it for every normal
|
|
56
|
+
* tester-facing caller; behavior is unchanged from before this existed.
|
|
50
57
|
*/
|
|
51
|
-
export function usePlaytestResponse(
|
|
58
|
+
export function usePlaytestResponse(
|
|
59
|
+
campaignId: number,
|
|
60
|
+
options?: { eligibilityFetcher?: () => Promise<PlaytestCampaign | null> },
|
|
61
|
+
) {
|
|
52
62
|
// ── Shared/global state ────────────────────────────────────────────────
|
|
53
63
|
const loading = ref(false);
|
|
54
64
|
const submitting = ref(false);
|
|
@@ -121,8 +131,9 @@ export function usePlaytestResponse(campaignId: number) {
|
|
|
121
131
|
async function loadCampaignEligibility() {
|
|
122
132
|
loading.value = true;
|
|
123
133
|
try {
|
|
124
|
-
const
|
|
125
|
-
|
|
134
|
+
const found = options?.eligibilityFetcher
|
|
135
|
+
? await options.eligibilityFetcher()
|
|
136
|
+
: (await fetchAvailablePlaytestCampaigns()).data.find((c) => c.id === campaignId) ?? null;
|
|
126
137
|
campaign.value = found;
|
|
127
138
|
if (found && found.eligible === false) {
|
|
128
139
|
ineligible.value = true;
|
package/locales/de.json
CHANGED
|
@@ -84,8 +84,10 @@
|
|
|
84
84
|
},
|
|
85
85
|
"flow": {
|
|
86
86
|
"criteria_completed": "Kriterien abgeschlossen",
|
|
87
|
+
"preview_banner": "Nur Vorschau — genau das sieht ein Tester. Hier kann nichts gesendet werden.",
|
|
87
88
|
"submit_checklist": "Checkliste absenden",
|
|
88
|
-
"submit_response": "Antwort absenden"
|
|
89
|
+
"submit_response": "Antwort absenden",
|
|
90
|
+
"video_preview_unavailable": "Hier zeichnen Tester beim Spielen ihren Bildschirm auf. Kamera-/Mikrofonaufnahme ist in der Vorschau nicht verfügbar."
|
|
89
91
|
},
|
|
90
92
|
"lqa": {
|
|
91
93
|
"actual_text": "Tatsächlicher Text",
|
|
@@ -389,6 +391,7 @@
|
|
|
389
391
|
"already_requested": "Sie haben bereits eine Anfrage für diese Kampagne gestellt.",
|
|
390
392
|
"sold_out": "Alle Keys für diese Kampagne wurden bereits eingelöst.",
|
|
391
393
|
"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.",
|
|
394
|
+
"countries_only": "Nur in {countries}",
|
|
392
395
|
"keys_left": "{n} Key übrig | {n} Keys übrig",
|
|
393
396
|
"keys_low_stock": "Letzter Key | Letzte {n}",
|
|
394
397
|
"back_to_campaign": "Zur Kampagne zurückkehren",
|
package/locales/en.json
CHANGED
|
@@ -84,8 +84,10 @@
|
|
|
84
84
|
},
|
|
85
85
|
"flow": {
|
|
86
86
|
"criteria_completed": "criteria completed",
|
|
87
|
+
"preview_banner": "Preview only — this is exactly what a tester sees. Nothing here can be submitted.",
|
|
87
88
|
"submit_checklist": "Submit checklist",
|
|
88
|
-
"submit_response": "Submit response"
|
|
89
|
+
"submit_response": "Submit response",
|
|
90
|
+
"video_preview_unavailable": "Testers record their screen here while playing. Camera/mic capture is not available in preview."
|
|
89
91
|
},
|
|
90
92
|
"lqa": {
|
|
91
93
|
"actual_text": "Actual text",
|
|
@@ -389,6 +391,7 @@
|
|
|
389
391
|
"already_requested": "You have already submitted a request for this campaign.",
|
|
390
392
|
"sold_out": "All keys for this campaign have already been claimed.",
|
|
391
393
|
"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.",
|
|
394
|
+
"countries_only": "Only in {countries}",
|
|
392
395
|
"keys_left": "{n} key left | {n} keys left",
|
|
393
396
|
"keys_low_stock": "Last key | Last {n}",
|
|
394
397
|
"back_to_campaign": "Back to campaign",
|
package/locales/es.json
CHANGED
|
@@ -84,8 +84,10 @@
|
|
|
84
84
|
},
|
|
85
85
|
"flow": {
|
|
86
86
|
"criteria_completed": "criterios completados",
|
|
87
|
+
"preview_banner": "Solo vista previa — esto es exactamente lo que ve un tester. Nada aquí se puede enviar.",
|
|
87
88
|
"submit_checklist": "Enviar checklist",
|
|
88
|
-
"submit_response": "Enviar respuesta"
|
|
89
|
+
"submit_response": "Enviar respuesta",
|
|
90
|
+
"video_preview_unavailable": "Aquí es donde los testers graban su pantalla mientras juegan. La captura de cámara/micrófono no está disponible en la vista previa."
|
|
89
91
|
},
|
|
90
92
|
"lqa": {
|
|
91
93
|
"actual_text": "Texto actual",
|
|
@@ -389,6 +391,7 @@
|
|
|
389
391
|
"already_requested": "Ya has enviado una solicitud para esta campaña.",
|
|
390
392
|
"sold_out": "Todas las claves de esta campaña ya fueron reclamadas.",
|
|
391
393
|
"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.",
|
|
394
|
+
"countries_only": "Solo en {countries}",
|
|
392
395
|
"keys_left": "{n} clave restante | {n} claves restantes",
|
|
393
396
|
"keys_low_stock": "Última clave | Últimas {n}",
|
|
394
397
|
"back_to_campaign": "Volver a la campaña",
|
package/locales/pt-BR.json
CHANGED
|
@@ -84,8 +84,10 @@
|
|
|
84
84
|
},
|
|
85
85
|
"flow": {
|
|
86
86
|
"criteria_completed": "critérios concluídos",
|
|
87
|
+
"preview_banner": "Somente pré-visualização — é exatamente isso que um tester vê. Nada aqui pode ser enviado.",
|
|
87
88
|
"submit_checklist": "Enviar checklist",
|
|
88
|
-
"submit_response": "Enviar resposta"
|
|
89
|
+
"submit_response": "Enviar resposta",
|
|
90
|
+
"video_preview_unavailable": "Aqui é onde os testers gravam a tela enquanto jogam. A captura de câmera/microfone não está disponível na pré-visualização."
|
|
89
91
|
},
|
|
90
92
|
"lqa": {
|
|
91
93
|
"actual_text": "Texto atual",
|
|
@@ -389,6 +391,7 @@
|
|
|
389
391
|
"already_requested": "Você já enviou uma solicitação para esta campanha.",
|
|
390
392
|
"sold_out": "Todas as chaves desta campanha já foram resgatadas.",
|
|
391
393
|
"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.",
|
|
394
|
+
"countries_only": "Somente em {countries}",
|
|
392
395
|
"keys_left": "{n} chave restante | {n} chaves restantes",
|
|
393
396
|
"keys_low_stock": "Última chave | Últimas {n}",
|
|
394
397
|
"back_to_campaign": "Voltar para a campanha",
|
package/locales/ro.json
CHANGED
|
@@ -84,8 +84,10 @@
|
|
|
84
84
|
},
|
|
85
85
|
"flow": {
|
|
86
86
|
"criteria_completed": "criterii finalizate",
|
|
87
|
+
"preview_banner": "Doar previzualizare — exact așa vede un tester. Nimic de aici nu poate fi trimis.",
|
|
87
88
|
"submit_checklist": "Trimite checklist-ul",
|
|
88
|
-
"submit_response": "Trimite răspunsul"
|
|
89
|
+
"submit_response": "Trimite răspunsul",
|
|
90
|
+
"video_preview_unavailable": "Aici testerii își înregistrează ecranul în timp ce joacă. Captura camerei/microfonului nu este disponibilă în previzualizare."
|
|
89
91
|
},
|
|
90
92
|
"lqa": {
|
|
91
93
|
"actual_text": "Text real",
|
|
@@ -389,6 +391,7 @@
|
|
|
389
391
|
"already_requested": "Ați trimis deja o cerere pentru această campanie.",
|
|
390
392
|
"sold_out": "Toate cheile pentru această campanie au fost deja revendicate.",
|
|
391
393
|
"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.",
|
|
394
|
+
"countries_only": "Doar în {countries}",
|
|
392
395
|
"keys_left": "{n} cheie rămasă | {n} chei rămase",
|
|
393
396
|
"keys_low_stock": "Ultima cheie | Ultimele {n}",
|
|
394
397
|
"back_to_campaign": "Înapoi la campanie",
|
package/package.json
CHANGED
|
@@ -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 {
|