@mundogamernetwork/shared-ui 1.16.21 → 1.16.23
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/indie-wall/MediaKitWallBlock.vue +1 -1
- package/components/playtest/build-access/BuildAccessPanel.vue +96 -0
- package/components/playtest/build-access/BuildDownload.vue +124 -0
- package/components/playtest/build-access/NdaSign.vue +221 -0
- package/components/playtest/concept-poll/FiveSecondTestVote.vue +1 -1
- package/components/playtest/lqa/FileIssueForm.vue +9 -1
- package/composables/usePlaytestBuildAccess.ts +219 -0
- package/locales/de.json +133 -0
- package/locales/en.json +133 -0
- package/locales/es.json +133 -0
- package/locales/pt-BR.json +133 -0
- package/locales/ro.json +133 -0
- package/package.json +1 -1
- package/pages/key-campaigns/redeem-key-approved.vue +51 -0
- package/pages/mural/[slug]/index.vue +148 -60
- package/plugins/echo.client.ts +49 -9
- package/services/esignatureService.ts +56 -0
- package/services/playtestTesterService.ts +53 -0
- package/pages/mural/[slug]/pixel/[pixelId].vue +0 -456
- package/pages/wall/[slug]/pixel/[pixelId].vue +0 -12
|
@@ -47,7 +47,7 @@ const wallUrl = computed(() => {
|
|
|
47
47
|
|
|
48
48
|
const detailPixelPageUrl = computed(() => {
|
|
49
49
|
if (!wall.value || !detailPixel.value) return '#'
|
|
50
|
-
return
|
|
50
|
+
return `${wallUrl.value}?pixel=${detailPixel.value.id}`
|
|
51
51
|
})
|
|
52
52
|
|
|
53
53
|
function onTapEmpty(coord: { x: number; y: number }) {
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Single orchestrator for a campaign's NDA-gated build access — mounted once
|
|
3
|
+
// per campaign a tester is eligible for, mirroring AccessGrantPanel.vue's
|
|
4
|
+
// role for the (unrelated, accept-time) key/link grant. Renders nothing if
|
|
5
|
+
// the campaign has no ready build asset at all, so campaigns that don't use
|
|
6
|
+
// this feature (concept polls, etc.) show no empty panel.
|
|
7
|
+
import { onMounted, computed } from "vue";
|
|
8
|
+
import { usePlaytestBuildAccess } from "../../../composables/usePlaytestBuildAccess";
|
|
9
|
+
import NdaSign from "./NdaSign.vue";
|
|
10
|
+
import BuildDownload from "./BuildDownload.vue";
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
campaignId: number;
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const state = usePlaytestBuildAccess(props.campaignId);
|
|
17
|
+
|
|
18
|
+
const hasAnyBuild = computed(() => state.assets.value.length > 0);
|
|
19
|
+
|
|
20
|
+
onMounted(async () => {
|
|
21
|
+
await state.loadAssets();
|
|
22
|
+
if (hasAnyBuild.value) {
|
|
23
|
+
await state.requestAccess();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
async function onDownload(assetId: number) {
|
|
28
|
+
const url = await state.getDownloadUrl(assetId);
|
|
29
|
+
if (url) window.open(url, "_blank", "noopener");
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<template>
|
|
34
|
+
<div v-if="state.loading.value" class="build-access-panel build-access-panel--loading">
|
|
35
|
+
<div class="build-access-panel__skeleton" />
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div v-else-if="hasAnyBuild" class="build-access-panel">
|
|
39
|
+
<NdaSign
|
|
40
|
+
v-if="state.grant.value?.status === 'pending_nda'"
|
|
41
|
+
:envelope="state.envelope.value"
|
|
42
|
+
:signer="state.signer.value"
|
|
43
|
+
:submitting="state.submitting.value"
|
|
44
|
+
:otp-initiated="state.otpInitiated.value"
|
|
45
|
+
:error="state.genericError.value"
|
|
46
|
+
@send-code="state.sendCode"
|
|
47
|
+
@verify="state.verifyOtp"
|
|
48
|
+
@accept-consent="state.acceptConsent"
|
|
49
|
+
@sign="state.sign"
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<BuildDownload
|
|
53
|
+
v-else-if="state.grant.value?.status === 'nda_signed'"
|
|
54
|
+
:assets="state.assets.value"
|
|
55
|
+
:submitting="state.submitting.value"
|
|
56
|
+
:error="state.genericError.value"
|
|
57
|
+
@download="onDownload"
|
|
58
|
+
/>
|
|
59
|
+
|
|
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>
|
|
63
|
+
</div>
|
|
64
|
+
</template>
|
|
65
|
+
|
|
66
|
+
<style scoped lang="scss">
|
|
67
|
+
.build-access-panel {
|
|
68
|
+
padding: 16px;
|
|
69
|
+
background: var(--bg-app-badge);
|
|
70
|
+
border: 1px solid var(--button-secondary-default-bg);
|
|
71
|
+
|
|
72
|
+
&--loading {
|
|
73
|
+
padding: 0;
|
|
74
|
+
border: none;
|
|
75
|
+
background: none;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
&__skeleton {
|
|
79
|
+
width: 100%;
|
|
80
|
+
height: 64px;
|
|
81
|
+
background: var(--bg-app-badge);
|
|
82
|
+
animation: build-access-panel-pulse 1.4s ease-in-out infinite;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
&__revoked {
|
|
86
|
+
font-size: 13px;
|
|
87
|
+
color: var(--danger, #e53935);
|
|
88
|
+
margin: 0;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
@keyframes build-access-panel-pulse {
|
|
93
|
+
0%, 100% { opacity: 1; }
|
|
94
|
+
50% { opacity: 0.4; }
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Lists a campaign's ready build assets, each with a Download button that
|
|
3
|
+
// calls the download endpoint FRESH every click — the returned URL is a
|
|
4
|
+
// 5-minute presigned S3 link and is never cached/reused, unlike
|
|
5
|
+
// AccessGrantReveal's masked-value-already-in-hand pattern (that value
|
|
6
|
+
// never expires; this one deliberately does).
|
|
7
|
+
import type { PlaytestBuildAsset } from "../../../services/playtestTesterService";
|
|
8
|
+
|
|
9
|
+
defineProps<{
|
|
10
|
+
assets: PlaytestBuildAsset[];
|
|
11
|
+
submitting: boolean;
|
|
12
|
+
error?: string | null;
|
|
13
|
+
}>();
|
|
14
|
+
|
|
15
|
+
const emit = defineEmits<{
|
|
16
|
+
(e: "download", assetId: number): void;
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
function formatSize(bytes: number | null): string {
|
|
20
|
+
if (!bytes) return "";
|
|
21
|
+
const mb = bytes / (1024 * 1024);
|
|
22
|
+
return mb >= 1024 ? `${(mb / 1024).toFixed(1)} GB` : `${mb.toFixed(1)} MB`;
|
|
23
|
+
}
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<template>
|
|
27
|
+
<div class="build-download">
|
|
28
|
+
<h3 class="build-download__title">
|
|
29
|
+
{{ $t("playtest.build_access.builds_title", "Available builds") }}
|
|
30
|
+
</h3>
|
|
31
|
+
|
|
32
|
+
<p v-if="error" class="build-download__error">{{ error }}</p>
|
|
33
|
+
|
|
34
|
+
<div v-if="!assets.length" class="build-download__empty">
|
|
35
|
+
{{ $t("playtest.build_access.no_builds", "No build has been uploaded yet.") }}
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div v-else class="build-download__list">
|
|
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>
|
|
45
|
+
</div>
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
class="btn primary"
|
|
49
|
+
:disabled="submitting"
|
|
50
|
+
@click="emit('download', asset.id)"
|
|
51
|
+
>
|
|
52
|
+
{{ $t("playtest.build_access.download", "Download") }}
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
|
|
59
|
+
<style scoped lang="scss">
|
|
60
|
+
.build-download {
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 10px;
|
|
64
|
+
|
|
65
|
+
&__title {
|
|
66
|
+
font-size: 15px;
|
|
67
|
+
font-weight: 700;
|
|
68
|
+
color: var(--card-article-title, #fff);
|
|
69
|
+
margin: 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&__empty {
|
|
73
|
+
font-size: 13px;
|
|
74
|
+
color: var(--secondary-info-fg, #888);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
&__error {
|
|
78
|
+
font-size: 12px;
|
|
79
|
+
color: var(--danger, #e53935);
|
|
80
|
+
margin: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
&__list {
|
|
84
|
+
display: flex;
|
|
85
|
+
flex-direction: column;
|
|
86
|
+
gap: 8px;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
&__row {
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: space-between;
|
|
93
|
+
gap: 12px;
|
|
94
|
+
padding: 10px 12px;
|
|
95
|
+
background: var(--bg-app-badge, #1a1a1a);
|
|
96
|
+
border: 1px solid var(--bg-app-badge);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
&__info {
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-wrap: wrap;
|
|
102
|
+
align-items: baseline;
|
|
103
|
+
gap: 8px;
|
|
104
|
+
font-size: 13px;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
&__platform {
|
|
108
|
+
text-transform: uppercase;
|
|
109
|
+
font-size: 10px;
|
|
110
|
+
font-weight: 700;
|
|
111
|
+
color: var(--primary, #d297ff);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
&__filename {
|
|
115
|
+
color: var(--card-article-title, #fff);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
&__version,
|
|
119
|
+
&__size {
|
|
120
|
+
color: var(--secondary-info-fg, #888);
|
|
121
|
+
font-size: 11px;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// NDA review + sign — document (text or embedded PDF) -> OTP -> consent ->
|
|
3
|
+
// sign. Purely presentational, mirrors AccessGrantWaiver.vue's role:
|
|
4
|
+
// BuildAccessPanel.vue owns the actual usePlaytestBuildAccess() calls. Four
|
|
5
|
+
// steps instead of AccessGrantWaiver's two because a real NDA needs an
|
|
6
|
+
// explicit clickwrap consent step separate from "I have the right code" —
|
|
7
|
+
// the old liability-waiver flow this replaces skipped that.
|
|
8
|
+
import { ref, computed } from "vue";
|
|
9
|
+
import type { EsignatureEnvelope, EsignatureSignerState } from "../../../services/esignatureService";
|
|
10
|
+
|
|
11
|
+
const props = defineProps<{
|
|
12
|
+
envelope: EsignatureEnvelope | null;
|
|
13
|
+
signer: EsignatureSignerState | null;
|
|
14
|
+
submitting: boolean;
|
|
15
|
+
otpInitiated: boolean;
|
|
16
|
+
error?: string | null;
|
|
17
|
+
}>();
|
|
18
|
+
|
|
19
|
+
const emit = defineEmits<{
|
|
20
|
+
(e: "send-code"): void;
|
|
21
|
+
(e: "verify", otpCode: string): void;
|
|
22
|
+
(e: "accept-consent"): void;
|
|
23
|
+
(e: "sign"): void;
|
|
24
|
+
}>();
|
|
25
|
+
|
|
26
|
+
const otpCode = ref("");
|
|
27
|
+
const consentChecked = ref(false);
|
|
28
|
+
|
|
29
|
+
const step = computed<"otp" | "consent" | "sign">(() => {
|
|
30
|
+
if (!props.signer?.otp_verified) return "otp";
|
|
31
|
+
if (props.signer.status !== "consented" && props.signer.status !== "signed") return "consent";
|
|
32
|
+
return "sign";
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
function submitCode() {
|
|
36
|
+
if (!otpCode.value) return;
|
|
37
|
+
emit("verify", otpCode.value);
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<div class="nda-sign">
|
|
43
|
+
<h3 class="nda-sign__title">
|
|
44
|
+
{{ $t("playtest.build_access.nda_title", "Sign the NDA to access this build") }}
|
|
45
|
+
</h3>
|
|
46
|
+
|
|
47
|
+
<div class="nda-sign__document">
|
|
48
|
+
<div v-if="envelope?.content_type === 'html'" class="nda-sign__document-html" v-html="envelope.document_html" />
|
|
49
|
+
<div v-else-if="envelope?.content_type === 'file' && envelope.document_file_preview_url" class="nda-sign__document-file">
|
|
50
|
+
<a :href="envelope.document_file_preview_url" target="_blank" rel="noopener">
|
|
51
|
+
{{ $t("playtest.build_access.view_pdf", "View the NDA document (PDF)") }} ↗
|
|
52
|
+
</a>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<p v-if="error" class="nda-sign__error">{{ error }}</p>
|
|
57
|
+
|
|
58
|
+
<!-- Step 1: OTP -->
|
|
59
|
+
<div v-if="step === 'otp'" class="nda-sign__step">
|
|
60
|
+
<div v-if="!otpInitiated" class="nda-sign__actions">
|
|
61
|
+
<button type="button" class="btn primary" :disabled="submitting" @click="emit('send-code')">
|
|
62
|
+
{{ submitting
|
|
63
|
+
? $t("playtest.build_access.sending_code", "Sending code…")
|
|
64
|
+
: $t("playtest.build_access.send_code", "Send verification code") }}
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
<div v-else class="nda-sign__otp">
|
|
68
|
+
<label class="nda-sign__label" for="nda-otp">
|
|
69
|
+
{{ $t("playtest.build_access.enter_code", "Enter the 6-digit code we emailed you") }}
|
|
70
|
+
</label>
|
|
71
|
+
<div class="nda-sign__otp-row">
|
|
72
|
+
<input
|
|
73
|
+
id="nda-otp"
|
|
74
|
+
v-model="otpCode"
|
|
75
|
+
type="text"
|
|
76
|
+
inputmode="numeric"
|
|
77
|
+
maxlength="6"
|
|
78
|
+
class="nda-sign__otp-input"
|
|
79
|
+
:disabled="submitting"
|
|
80
|
+
@keyup.enter="submitCode"
|
|
81
|
+
/>
|
|
82
|
+
<button type="button" class="btn primary" :disabled="submitting || otpCode.length === 0" @click="submitCode">
|
|
83
|
+
{{ submitting ? $t("playtest.build_access.verifying", "Verifying…") : $t("playtest.build_access.verify", "Verify") }}
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
<button type="button" class="nda-sign__resend" :disabled="submitting" @click="emit('send-code')">
|
|
87
|
+
{{ $t("playtest.build_access.resend_code", "Resend code") }}
|
|
88
|
+
</button>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- Step 2: Consent -->
|
|
93
|
+
<div v-else-if="step === 'consent'" class="nda-sign__step">
|
|
94
|
+
<label class="nda-sign__consent-row">
|
|
95
|
+
<input type="checkbox" v-model="consentChecked" :disabled="submitting" />
|
|
96
|
+
{{ $t("playtest.build_access.consent_checkbox", "I have read and agree to the terms of this NDA") }}
|
|
97
|
+
</label>
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
class="btn primary"
|
|
101
|
+
:disabled="submitting || !consentChecked"
|
|
102
|
+
@click="emit('accept-consent')"
|
|
103
|
+
>
|
|
104
|
+
{{ submitting ? $t("playtest.build_access.accepting", "Accepting…") : $t("playtest.build_access.accept_consent", "Accept") }}
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<!-- Step 3: Sign -->
|
|
109
|
+
<div v-else class="nda-sign__step">
|
|
110
|
+
<button type="button" class="btn primary" :disabled="submitting" @click="emit('sign')">
|
|
111
|
+
{{ submitting ? $t("playtest.build_access.signing", "Signing…") : $t("playtest.build_access.sign", "Sign NDA") }}
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
116
|
+
|
|
117
|
+
<style scoped lang="scss">
|
|
118
|
+
.nda-sign {
|
|
119
|
+
display: flex;
|
|
120
|
+
flex-direction: column;
|
|
121
|
+
gap: 14px;
|
|
122
|
+
|
|
123
|
+
&__title {
|
|
124
|
+
font-size: 15px;
|
|
125
|
+
font-weight: 700;
|
|
126
|
+
color: var(--card-article-title, #fff);
|
|
127
|
+
margin: 0;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
&__document {
|
|
131
|
+
max-height: 260px;
|
|
132
|
+
overflow-y: auto;
|
|
133
|
+
padding: 12px;
|
|
134
|
+
background: var(--bg-app-badge, #1a1a1a);
|
|
135
|
+
border: 1px solid var(--bg-app-badge);
|
|
136
|
+
font-size: 12px;
|
|
137
|
+
color: var(--secondary-info-fg, #ccc);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&__document-html {
|
|
141
|
+
:deep(p) { margin: 0 0 8px; }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
&__document-file a {
|
|
145
|
+
color: var(--primary, #d297ff);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
&__error {
|
|
149
|
+
font-size: 12px;
|
|
150
|
+
color: var(--danger, #e53935);
|
|
151
|
+
margin: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
&__step {
|
|
155
|
+
display: flex;
|
|
156
|
+
flex-direction: column;
|
|
157
|
+
gap: 10px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
&__actions {
|
|
161
|
+
display: flex;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
&__otp {
|
|
165
|
+
display: flex;
|
|
166
|
+
flex-direction: column;
|
|
167
|
+
gap: 8px;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
&__label {
|
|
171
|
+
font-size: 12px;
|
|
172
|
+
color: var(--secondary-info-fg, #aaa);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
&__otp-row {
|
|
176
|
+
display: flex;
|
|
177
|
+
align-items: center;
|
|
178
|
+
gap: 8px;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
&__otp-input {
|
|
182
|
+
width: 140px;
|
|
183
|
+
height: 44px;
|
|
184
|
+
padding: 0 12px;
|
|
185
|
+
background-color: transparent;
|
|
186
|
+
border: 1px solid var(--bg-app-badge);
|
|
187
|
+
color: var(--card-article-title, #fff);
|
|
188
|
+
font-size: 18px;
|
|
189
|
+
letter-spacing: 4px;
|
|
190
|
+
text-align: center;
|
|
191
|
+
|
|
192
|
+
&:focus {
|
|
193
|
+
border-color: var(--primary, #d297ff);
|
|
194
|
+
outline: 0;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
&__resend {
|
|
199
|
+
align-self: flex-start;
|
|
200
|
+
background: none;
|
|
201
|
+
border: none;
|
|
202
|
+
padding: 0;
|
|
203
|
+
font-size: 12px;
|
|
204
|
+
color: var(--primary, #d297ff);
|
|
205
|
+
cursor: pointer;
|
|
206
|
+
|
|
207
|
+
&:disabled {
|
|
208
|
+
opacity: 0.45;
|
|
209
|
+
cursor: not-allowed;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
&__consent-row {
|
|
214
|
+
display: flex;
|
|
215
|
+
align-items: center;
|
|
216
|
+
gap: 8px;
|
|
217
|
+
font-size: 13px;
|
|
218
|
+
color: var(--secondary-info-fg, #ccc);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
</style>
|
|
@@ -70,7 +70,7 @@ function submit() {
|
|
|
70
70
|
<template>
|
|
71
71
|
<div class="five-sec-vote">
|
|
72
72
|
<div v-if="phase === 'intro'" class="five-sec-vote__intro">
|
|
73
|
-
<p>{{ $t("playtest.concept_poll.five_second_intro", `You'll see the image(s) for ${seconds} seconds, then answer from memory
|
|
73
|
+
<p>{{ $t("playtest.concept_poll.five_second_intro", `You'll see the image(s) for ${seconds} seconds, then answer from memory.`, { seconds }) }}</p>
|
|
74
74
|
<button type="button" class="btn primary" @click="start">
|
|
75
75
|
{{ $t("playtest.concept_poll.five_second_start", "Start") }}
|
|
76
76
|
</button>
|
|
@@ -38,6 +38,7 @@ const canSubmit = computed(() =>
|
|
|
38
38
|
!!issueType.value &&
|
|
39
39
|
!!severity.value &&
|
|
40
40
|
description.value.trim().length > 0 &&
|
|
41
|
+
!!mediaUrl.value &&
|
|
41
42
|
!props.submitting
|
|
42
43
|
);
|
|
43
44
|
|
|
@@ -116,10 +117,11 @@ function submit() {
|
|
|
116
117
|
</div>
|
|
117
118
|
|
|
118
119
|
<div class="file-issue-form__row">
|
|
119
|
-
<label class="file-issue-form__label">{{ $t("playtest.lqa.attach_screenshot", "Attach screenshot/video (
|
|
120
|
+
<label class="file-issue-form__label">{{ $t("playtest.lqa.attach_screenshot", "Attach screenshot/video (required)") }}</label>
|
|
120
121
|
<input type="file" accept="image/*,video/mp4,video/quicktime,video/webm" @change="onFileChange">
|
|
121
122
|
<span v-if="uploading" class="file-issue-form__uploading">{{ $t("playtest.lqa.uploading", "Uploading…") }}</span>
|
|
122
123
|
<span v-else-if="mediaUrl" class="file-issue-form__uploaded">{{ $t("playtest.lqa.uploaded", "Attached") }}</span>
|
|
124
|
+
<p v-if="!mediaUrl && !uploading" class="file-issue-form__hint">{{ $t("playtest.lqa.evidence_hint", "A screenshot or clip of the actual build is required so the studio can verify the issue.") }}</p>
|
|
123
125
|
</div>
|
|
124
126
|
|
|
125
127
|
<button type="button" class="btn primary file-issue-form__submit" :disabled="!canSubmit" @click="submit">
|
|
@@ -174,6 +176,12 @@ function submit() {
|
|
|
174
176
|
color: var(--primary, #D297FF);
|
|
175
177
|
}
|
|
176
178
|
|
|
179
|
+
&__hint {
|
|
180
|
+
margin: 6px 0 0;
|
|
181
|
+
font-size: 11px;
|
|
182
|
+
color: var(--secondary-info-fg, #888);
|
|
183
|
+
}
|
|
184
|
+
|
|
177
185
|
&__submit {
|
|
178
186
|
margin-top: 4px;
|
|
179
187
|
}
|