@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.
- package/components/playtest/build-access/BuildAccessPanel.vue +127 -4
- package/components/playtest/build-access/BuildDownload.vue +22 -5
- package/components/playtest/build-access/ReportProblemForm.vue +143 -0
- package/composables/usePlaytestBuildAccess.ts +44 -0
- package/locales/de.json +19 -1
- package/locales/en.json +19 -1
- package/locales/es.json +19 -1
- package/locales/pt-BR.json +19 -1
- package/locales/ro.json +19 -1
- package/package.json +1 -1
- package/pages/key-campaigns/index.vue +31 -1
- package/services/playtestTesterService.ts +38 -0
|
@@ -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
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
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-
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
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
|
|
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
|
|
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",
|
package/locales/pt-BR.json
CHANGED
|
@@ -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
|
|
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
|
@@ -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
|
+
)
|