@mundogamernetwork/shared-ui 1.8.23 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/keys/KeyBrowser.vue +31 -1
- package/components/keys/KeyCampaignCommentItem.vue +303 -0
- package/components/keys/KeyCampaignComments.vue +355 -0
- package/components/keys/KeyCampaignFeedback.vue +250 -0
- package/locales/de.json +45 -3
- package/locales/en.json +45 -3
- package/locales/es.json +45 -3
- package/locales/pt-BR.json +45 -3
- package/locales/ro.json +45 -3
- package/package.json +1 -1
- package/pages/key-campaigns/[slug].vue +86 -34
- package/pages/key-campaigns/index.vue +72 -1
- package/pages/key-campaigns/redeem-key-approved.vue +22 -13
- package/pages/key-campaigns/redeem-key.vue +56 -8
- package/pages/presskit/game/[slug].vue +33 -3
- package/services/campaignService.ts +149 -2
- package/utils/embargo.ts +47 -18
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section id="comments" class="campaign-comments">
|
|
3
|
+
<header class="campaign-comments__header">
|
|
4
|
+
<h3 class="campaign-comments__title">
|
|
5
|
+
{{ $t("keys.campaigns.comments.title") }}
|
|
6
|
+
<span v-if="total" class="campaign-comments__count">{{ total }}</span>
|
|
7
|
+
</h3>
|
|
8
|
+
<p class="campaign-comments__subtitle">
|
|
9
|
+
{{ $t("keys.campaigns.comments.subtitle") }}
|
|
10
|
+
</p>
|
|
11
|
+
</header>
|
|
12
|
+
|
|
13
|
+
<!-- Composer. Rendered only when the API says this viewer may write, so
|
|
14
|
+
the rules live in one place instead of being re-derived here. -->
|
|
15
|
+
<form v-if="canComment" class="campaign-comments__composer" @submit.prevent="submit">
|
|
16
|
+
<textarea
|
|
17
|
+
v-model="draft"
|
|
18
|
+
class="campaign-comments__input"
|
|
19
|
+
:placeholder="$t('keys.campaigns.comments.placeholder')"
|
|
20
|
+
:maxlength="2200"
|
|
21
|
+
rows="3"
|
|
22
|
+
:disabled="posting"
|
|
23
|
+
/>
|
|
24
|
+
<div class="campaign-comments__composer-footer">
|
|
25
|
+
<span class="campaign-comments__counter">{{ draft.length }}/2200</span>
|
|
26
|
+
<button type="submit" class="campaign-comments__submit" :disabled="!canSubmit">
|
|
27
|
+
{{ posting ? $t("keys.campaigns.comments.sending") : $t("keys.campaigns.comments.send") }}
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
</form>
|
|
31
|
+
|
|
32
|
+
<p v-else-if="!signedIn" class="campaign-comments__notice">
|
|
33
|
+
{{ $t("keys.campaigns.comments.sign_in_notice") }}
|
|
34
|
+
</p>
|
|
35
|
+
<p v-else class="campaign-comments__notice">
|
|
36
|
+
{{ $t("keys.campaigns.comments.creators_only_notice") }}
|
|
37
|
+
</p>
|
|
38
|
+
|
|
39
|
+
<p v-if="error" class="campaign-comments__error">{{ error }}</p>
|
|
40
|
+
|
|
41
|
+
<div v-if="loading && !comments.length" class="campaign-comments__skeleton">
|
|
42
|
+
<div v-for="n in 3" :key="n" class="campaign-comments__skeleton-row" />
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- The empty state is a statement of fact ("nobody has posted yet"), so
|
|
46
|
+
it must not show when the list is empty merely because the request
|
|
47
|
+
failed — the error message above already says what happened. -->
|
|
48
|
+
<p v-else-if="!comments.length && !error" class="campaign-comments__empty">
|
|
49
|
+
{{ $t("keys.campaigns.comments.empty") }}
|
|
50
|
+
</p>
|
|
51
|
+
|
|
52
|
+
<ul v-else class="campaign-comments__list">
|
|
53
|
+
<li v-for="comment in comments" :key="comment.uuid" class="campaign-comments__item">
|
|
54
|
+
<KeyCampaignCommentItem
|
|
55
|
+
:comment="comment"
|
|
56
|
+
:can-moderate="canModerate"
|
|
57
|
+
:can-reply="canComment"
|
|
58
|
+
@edit="onEdit"
|
|
59
|
+
@delete="onDelete"
|
|
60
|
+
@reply="onReply"
|
|
61
|
+
@hide="onHide"
|
|
62
|
+
@unhide="onUnhide"
|
|
63
|
+
/>
|
|
64
|
+
</li>
|
|
65
|
+
</ul>
|
|
66
|
+
|
|
67
|
+
<button
|
|
68
|
+
v-if="hasMore"
|
|
69
|
+
class="campaign-comments__more"
|
|
70
|
+
:disabled="loading"
|
|
71
|
+
@click="loadMore"
|
|
72
|
+
>
|
|
73
|
+
{{ $t("keys.campaigns.comments.load_more") }}
|
|
74
|
+
</button>
|
|
75
|
+
</section>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<script setup lang="ts">
|
|
79
|
+
import { ref, computed, onMounted } from "vue"
|
|
80
|
+
import { useNuxtApp } from "#app"
|
|
81
|
+
import {
|
|
82
|
+
fetchCampaignComments,
|
|
83
|
+
postCampaignComment,
|
|
84
|
+
replyCampaignComment,
|
|
85
|
+
updateCampaignComment,
|
|
86
|
+
deleteCampaignComment,
|
|
87
|
+
hideCampaignComment,
|
|
88
|
+
unhideCampaignComment,
|
|
89
|
+
type CampaignComment,
|
|
90
|
+
} from "../../services/campaignService"
|
|
91
|
+
|
|
92
|
+
const props = defineProps<{
|
|
93
|
+
keyId: number
|
|
94
|
+
/**
|
|
95
|
+
* Permission flags come from the campaign payload (KeyTransformer) so the
|
|
96
|
+
* composer's visibility matches what the API will actually accept. They are
|
|
97
|
+
* refreshed from the comments response, which computes them too.
|
|
98
|
+
*/
|
|
99
|
+
canComment?: boolean
|
|
100
|
+
canModerate?: boolean
|
|
101
|
+
signedIn?: boolean
|
|
102
|
+
}>()
|
|
103
|
+
|
|
104
|
+
const { $i18n } = useNuxtApp()
|
|
105
|
+
|
|
106
|
+
const comments = ref<CampaignComment[]>([])
|
|
107
|
+
const page = ref(1)
|
|
108
|
+
const lastPage = ref(1)
|
|
109
|
+
const total = ref(0)
|
|
110
|
+
const loading = ref(false)
|
|
111
|
+
const posting = ref(false)
|
|
112
|
+
const error = ref("")
|
|
113
|
+
const draft = ref("")
|
|
114
|
+
|
|
115
|
+
const canComment = ref(props.canComment ?? false)
|
|
116
|
+
const canModerate = ref(props.canModerate ?? false)
|
|
117
|
+
|
|
118
|
+
const hasMore = computed(() => page.value < lastPage.value)
|
|
119
|
+
const canSubmit = computed(() => !posting.value && draft.value.trim().length >= 2)
|
|
120
|
+
|
|
121
|
+
async function load(targetPage = 1) {
|
|
122
|
+
loading.value = true
|
|
123
|
+
error.value = ""
|
|
124
|
+
try {
|
|
125
|
+
const { data } = await fetchCampaignComments(props.keyId, targetPage)
|
|
126
|
+
comments.value = targetPage === 1 ? data.data : [...comments.value, ...data.data]
|
|
127
|
+
page.value = data.meta.current_page
|
|
128
|
+
lastPage.value = data.meta.last_page
|
|
129
|
+
total.value = data.meta.total
|
|
130
|
+
canComment.value = data.can_comment
|
|
131
|
+
canModerate.value = data.can_moderate
|
|
132
|
+
} catch (_) {
|
|
133
|
+
// $i18n from useNuxtApp, not globalThis.$nuxt — the latter is undefined
|
|
134
|
+
// during SSR and on a fresh client load, which rendered the literal
|
|
135
|
+
// string "undefined" where the error message should be.
|
|
136
|
+
error.value = $i18n.t("keys.campaigns.comments.load_error")
|
|
137
|
+
} finally {
|
|
138
|
+
loading.value = false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const loadMore = () => load(page.value + 1)
|
|
143
|
+
|
|
144
|
+
async function submit() {
|
|
145
|
+
if (!canSubmit.value) return
|
|
146
|
+
posting.value = true
|
|
147
|
+
error.value = ""
|
|
148
|
+
try {
|
|
149
|
+
await postCampaignComment(props.keyId, draft.value.trim())
|
|
150
|
+
draft.value = ""
|
|
151
|
+
// Reload rather than unshifting locally: the server owns ordering and
|
|
152
|
+
// the permission flags, and a fresh page keeps them honest.
|
|
153
|
+
await load(1)
|
|
154
|
+
} catch (e: any) {
|
|
155
|
+
error.value = e?.response?.data?.message || e?.response?.data?.errors?.body?.[0] || ""
|
|
156
|
+
} finally {
|
|
157
|
+
posting.value = false
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function onReply({ uuid, body }: { uuid: string; body: string }) {
|
|
162
|
+
await replyCampaignComment(uuid, body)
|
|
163
|
+
await load(1)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function onEdit({ uuid, body }: { uuid: string; body: string }) {
|
|
167
|
+
await updateCampaignComment(uuid, body)
|
|
168
|
+
await load(page.value)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function onDelete(uuid: string) {
|
|
172
|
+
await deleteCampaignComment(uuid)
|
|
173
|
+
await load(1)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function onHide({ uuid, reason }: { uuid: string; reason?: string }) {
|
|
177
|
+
await hideCampaignComment(uuid, reason)
|
|
178
|
+
await load(page.value)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function onUnhide(uuid: string) {
|
|
182
|
+
await unhideCampaignComment(uuid)
|
|
183
|
+
await load(page.value)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
onMounted(() => load(1))
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<style lang="scss" scoped>
|
|
190
|
+
/* Theme tokens only — no literal colours. Every variable used here is defined
|
|
191
|
+
in both _light.scss and _dark.scss of agency-frontend, community-frontend and
|
|
192
|
+
tv-frontend, which are the fronts that render this page. Note the themes are
|
|
193
|
+
not "light = pale": agency's .light-mode sets --body-bg to #18181a, so a
|
|
194
|
+
hardcoded translucent-white surface would be invisible in one of them. */
|
|
195
|
+
.campaign-comments {
|
|
196
|
+
margin-top: 2.5rem;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.campaign-comments__header {
|
|
200
|
+
margin-bottom: 1.25rem;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.campaign-comments__title {
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
gap: 0.5rem;
|
|
207
|
+
font-size: 1.25rem;
|
|
208
|
+
font-weight: 700;
|
|
209
|
+
margin: 0;
|
|
210
|
+
color: var(--title-fg);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.campaign-comments__count {
|
|
214
|
+
font-size: 0.875rem;
|
|
215
|
+
font-weight: 600;
|
|
216
|
+
padding: 0.125rem 0.5rem;
|
|
217
|
+
color: var(--title-fg);
|
|
218
|
+
background: var(--card-article-bg);
|
|
219
|
+
border: 1px solid var(--card-article-border);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.campaign-comments__subtitle {
|
|
223
|
+
margin: 0.25rem 0 0;
|
|
224
|
+
font-size: 0.875rem;
|
|
225
|
+
color: var(--secondary-info-fg);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.campaign-comments__composer {
|
|
229
|
+
margin-bottom: 1.5rem;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.campaign-comments__input {
|
|
233
|
+
width: 100%;
|
|
234
|
+
padding: 0.75rem;
|
|
235
|
+
/* Deliberately NOT --input-bg/--input-fg: --input-bg is #CCCCCC in both
|
|
236
|
+
themes while --input-fg is #FFFFFF in dark, giving 1.61:1 — unreadable.
|
|
237
|
+
These two invert with the theme and measure above 4.5:1 in both. */
|
|
238
|
+
background: var(--card-article-bg);
|
|
239
|
+
border: 1px solid var(--input-border-color);
|
|
240
|
+
color: var(--title-fg);
|
|
241
|
+
font: inherit;
|
|
242
|
+
resize: vertical;
|
|
243
|
+
|
|
244
|
+
&::placeholder {
|
|
245
|
+
color: var(--secondary-info-fg);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
&:focus {
|
|
249
|
+
outline: none;
|
|
250
|
+
border-color: var(--key-accent, var(--primary));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.campaign-comments__composer-footer {
|
|
255
|
+
display: flex;
|
|
256
|
+
align-items: center;
|
|
257
|
+
justify-content: space-between;
|
|
258
|
+
gap: 0.75rem;
|
|
259
|
+
margin-top: 0.5rem;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.campaign-comments__counter {
|
|
263
|
+
font-size: 0.75rem;
|
|
264
|
+
color: var(--secondary-info-fg);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Matches the .btn convention of the campaign page itself. */
|
|
268
|
+
.campaign-comments__submit {
|
|
269
|
+
height: 44px;
|
|
270
|
+
padding: 0 1.25rem;
|
|
271
|
+
background: var(--key-accent, var(--primary));
|
|
272
|
+
color: #000;
|
|
273
|
+
border: 0;
|
|
274
|
+
font-weight: 600;
|
|
275
|
+
cursor: pointer;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.campaign-comments__submit:disabled {
|
|
279
|
+
opacity: 0.5;
|
|
280
|
+
cursor: not-allowed;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.campaign-comments__notice,
|
|
284
|
+
.campaign-comments__empty {
|
|
285
|
+
font-size: 0.875rem;
|
|
286
|
+
color: var(--secondary-info-fg);
|
|
287
|
+
margin: 0 0 1.25rem;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.campaign-comments__error {
|
|
291
|
+
font-size: 0.875rem;
|
|
292
|
+
color: var(--danger);
|
|
293
|
+
margin: 0 0 1rem;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.campaign-comments__list {
|
|
297
|
+
list-style: none;
|
|
298
|
+
margin: 0;
|
|
299
|
+
padding: 0;
|
|
300
|
+
display: grid;
|
|
301
|
+
gap: 1rem;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.campaign-comments__skeleton {
|
|
305
|
+
display: grid;
|
|
306
|
+
gap: 1rem;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* Skeleton, not spinner — the list shape is known before the data arrives. */
|
|
310
|
+
.campaign-comments__skeleton-row {
|
|
311
|
+
height: 4.5rem;
|
|
312
|
+
background: var(--card-article-bg);
|
|
313
|
+
animation: campaign-comments-pulse 1.4s ease-in-out infinite;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
@keyframes campaign-comments-pulse {
|
|
317
|
+
0%, 100% { opacity: 1; }
|
|
318
|
+
50% { opacity: 0.45; }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
@media (prefers-reduced-motion: reduce) {
|
|
322
|
+
.campaign-comments__skeleton-row { animation: none; }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.campaign-comments__more {
|
|
326
|
+
margin-top: 1.25rem;
|
|
327
|
+
height: 44px;
|
|
328
|
+
padding: 0 1rem;
|
|
329
|
+
background: transparent;
|
|
330
|
+
border: 1px solid var(--card-article-border);
|
|
331
|
+
color: var(--title-fg);
|
|
332
|
+
cursor: pointer;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* Same breakpoint the campaign page uses for its own stacking. */
|
|
336
|
+
@media (max-width: 768px) {
|
|
337
|
+
.campaign-comments {
|
|
338
|
+
margin-top: 1.75rem;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
.campaign-comments__title {
|
|
342
|
+
font-size: 1.125rem;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.campaign-comments__composer-footer {
|
|
346
|
+
flex-direction: column;
|
|
347
|
+
align-items: stretch;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.campaign-comments__submit,
|
|
351
|
+
.campaign-comments__more {
|
|
352
|
+
width: 100%;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
</style>
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<section v-if="canRate || submitted" class="campaign-feedback">
|
|
3
|
+
<header class="campaign-feedback__header">
|
|
4
|
+
<h3 class="campaign-feedback__title">{{ $t("keys.campaigns.feedback.title") }}</h3>
|
|
5
|
+
<p class="campaign-feedback__subtitle">
|
|
6
|
+
{{ $t("keys.campaigns.feedback.subtitle", { game: gameName }) }}
|
|
7
|
+
</p>
|
|
8
|
+
</header>
|
|
9
|
+
|
|
10
|
+
<p v-if="submitted" class="campaign-feedback__done">
|
|
11
|
+
{{ $t("keys.campaigns.feedback.submitted", { game: gameName }) }}
|
|
12
|
+
<a v-if="gameSlug" :href="gameUrl" class="campaign-feedback__link">
|
|
13
|
+
{{ $t("keys.campaigns.feedback.view_on_game_page") }}
|
|
14
|
+
</a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<form v-else class="campaign-feedback__form" @submit.prevent="submit">
|
|
18
|
+
<div class="campaign-feedback__stars" role="radiogroup" :aria-label="$t('keys.campaigns.feedback.rating_label')">
|
|
19
|
+
<button
|
|
20
|
+
v-for="star in 5"
|
|
21
|
+
:key="star"
|
|
22
|
+
type="button"
|
|
23
|
+
class="campaign-feedback__star"
|
|
24
|
+
:class="{ 'campaign-feedback__star--on': star <= rating }"
|
|
25
|
+
role="radio"
|
|
26
|
+
:aria-checked="star === rating"
|
|
27
|
+
:aria-label="String(star)"
|
|
28
|
+
@click="rating = star"
|
|
29
|
+
>
|
|
30
|
+
★
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<input
|
|
35
|
+
v-model="title"
|
|
36
|
+
type="text"
|
|
37
|
+
class="campaign-feedback__input"
|
|
38
|
+
:maxlength="191"
|
|
39
|
+
:placeholder="$t('keys.campaigns.feedback.title_placeholder')"
|
|
40
|
+
>
|
|
41
|
+
|
|
42
|
+
<textarea
|
|
43
|
+
v-model="body"
|
|
44
|
+
class="campaign-feedback__input campaign-feedback__textarea"
|
|
45
|
+
rows="4"
|
|
46
|
+
:maxlength="2200"
|
|
47
|
+
:placeholder="$t('keys.campaigns.feedback.body_placeholder')"
|
|
48
|
+
/>
|
|
49
|
+
|
|
50
|
+
<!-- Not decoration: the API rejects the submission without it, because
|
|
51
|
+
this rating counts towards the game's public average. -->
|
|
52
|
+
<label class="campaign-feedback__disclosure">
|
|
53
|
+
<input v-model="disclosure" type="checkbox">
|
|
54
|
+
<span>{{ $t("keys.campaigns.feedback.disclosure") }}</span>
|
|
55
|
+
</label>
|
|
56
|
+
|
|
57
|
+
<p class="campaign-feedback__note">{{ $t("keys.campaigns.feedback.average_note") }}</p>
|
|
58
|
+
|
|
59
|
+
<p v-if="error" class="campaign-feedback__error">{{ error }}</p>
|
|
60
|
+
|
|
61
|
+
<button type="submit" class="campaign-feedback__submit" :disabled="!canSubmit">
|
|
62
|
+
{{ sending ? $t("keys.campaigns.feedback.sending") : $t("keys.campaigns.feedback.send") }}
|
|
63
|
+
</button>
|
|
64
|
+
</form>
|
|
65
|
+
</section>
|
|
66
|
+
</template>
|
|
67
|
+
|
|
68
|
+
<script setup lang="ts">
|
|
69
|
+
import { ref, computed } from "vue"
|
|
70
|
+
import { submitCampaignGameRating } from "../../services/campaignService"
|
|
71
|
+
|
|
72
|
+
const props = defineProps<{
|
|
73
|
+
keyId: number
|
|
74
|
+
/** From the campaign payload — true only for creators who revealed a key. */
|
|
75
|
+
canRate?: boolean
|
|
76
|
+
gameName?: string
|
|
77
|
+
gameSlug?: string
|
|
78
|
+
locale?: string
|
|
79
|
+
}>()
|
|
80
|
+
|
|
81
|
+
const rating = ref(0)
|
|
82
|
+
const title = ref("")
|
|
83
|
+
const body = ref("")
|
|
84
|
+
const disclosure = ref(false)
|
|
85
|
+
const sending = ref(false)
|
|
86
|
+
const submitted = ref(false)
|
|
87
|
+
const error = ref("")
|
|
88
|
+
|
|
89
|
+
const gameName = computed(() => props.gameName || "")
|
|
90
|
+
const gameUrl = computed(() => `/${props.locale || "en"}/game/${props.gameSlug}?tab=ratings`)
|
|
91
|
+
|
|
92
|
+
const canSubmit = computed(() => !sending.value && rating.value > 0 && disclosure.value)
|
|
93
|
+
|
|
94
|
+
async function submit() {
|
|
95
|
+
if (!canSubmit.value) return
|
|
96
|
+
sending.value = true
|
|
97
|
+
error.value = ""
|
|
98
|
+
try {
|
|
99
|
+
await submitCampaignGameRating(props.keyId, {
|
|
100
|
+
rating: rating.value,
|
|
101
|
+
comment_title: title.value || undefined,
|
|
102
|
+
user_comment: body.value || undefined,
|
|
103
|
+
disclosure_acknowledged: disclosure.value,
|
|
104
|
+
})
|
|
105
|
+
submitted.value = true
|
|
106
|
+
} catch (e: any) {
|
|
107
|
+
error.value =
|
|
108
|
+
e?.response?.data?.message ||
|
|
109
|
+
e?.response?.data?.errors?.disclosure_acknowledged?.[0] ||
|
|
110
|
+
e?.response?.data?.errors?.rating?.[0] ||
|
|
111
|
+
""
|
|
112
|
+
} finally {
|
|
113
|
+
sending.value = false
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
</script>
|
|
117
|
+
|
|
118
|
+
<style lang="scss" scoped>
|
|
119
|
+
/* Theme tokens only — see KeyCampaignComments.vue for why no literal colours. */
|
|
120
|
+
.campaign-feedback {
|
|
121
|
+
margin-top: 2.5rem;
|
|
122
|
+
padding: 1.25rem;
|
|
123
|
+
background: var(--card-article-bg);
|
|
124
|
+
border: 1px solid var(--card-article-border);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.campaign-feedback__title {
|
|
128
|
+
margin: 0;
|
|
129
|
+
font-size: 1.125rem;
|
|
130
|
+
font-weight: 700;
|
|
131
|
+
color: var(--title-fg);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.campaign-feedback__subtitle {
|
|
135
|
+
margin: 0.25rem 0 1rem;
|
|
136
|
+
font-size: 0.875rem;
|
|
137
|
+
color: var(--secondary-info-fg);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.campaign-feedback__stars {
|
|
141
|
+
display: flex;
|
|
142
|
+
gap: 0.25rem;
|
|
143
|
+
margin-bottom: 0.875rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.campaign-feedback__star {
|
|
147
|
+
background: none;
|
|
148
|
+
border: 0;
|
|
149
|
+
padding: 0.125rem;
|
|
150
|
+
font-size: 1.75rem;
|
|
151
|
+
line-height: 1;
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
color: var(--secondary-info-fg);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.campaign-feedback__star--on {
|
|
157
|
+
color: var(--warning);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.campaign-feedback__input {
|
|
161
|
+
display: block;
|
|
162
|
+
width: 100%;
|
|
163
|
+
margin-bottom: 0.75rem;
|
|
164
|
+
padding: 0.625rem;
|
|
165
|
+
/* See KeyCampaignComments.vue: --input-bg/--input-fg is 1.61:1 in dark. */
|
|
166
|
+
background: var(--card-article-bg);
|
|
167
|
+
border: 1px solid var(--input-border-color);
|
|
168
|
+
color: var(--title-fg);
|
|
169
|
+
font: inherit;
|
|
170
|
+
|
|
171
|
+
&::placeholder {
|
|
172
|
+
color: var(--secondary-info-fg);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
&:focus {
|
|
176
|
+
outline: none;
|
|
177
|
+
border-color: var(--key-accent, var(--primary));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.campaign-feedback__textarea {
|
|
182
|
+
resize: vertical;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.campaign-feedback__disclosure {
|
|
186
|
+
display: flex;
|
|
187
|
+
align-items: flex-start;
|
|
188
|
+
gap: 0.5rem;
|
|
189
|
+
font-size: 0.8125rem;
|
|
190
|
+
margin-bottom: 0.5rem;
|
|
191
|
+
color: var(--card-article-title);
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.campaign-feedback__note {
|
|
196
|
+
margin: 0 0 0.875rem;
|
|
197
|
+
font-size: 0.75rem;
|
|
198
|
+
color: var(--secondary-info-fg);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.campaign-feedback__error {
|
|
202
|
+
margin: 0 0 0.75rem;
|
|
203
|
+
font-size: 0.8125rem;
|
|
204
|
+
color: var(--danger);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
.campaign-feedback__submit {
|
|
208
|
+
height: 44px;
|
|
209
|
+
padding: 0 1.25rem;
|
|
210
|
+
background: var(--key-accent, var(--primary));
|
|
211
|
+
color: #000;
|
|
212
|
+
border: 0;
|
|
213
|
+
font-weight: 600;
|
|
214
|
+
cursor: pointer;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.campaign-feedback__submit:disabled {
|
|
218
|
+
opacity: 0.5;
|
|
219
|
+
cursor: not-allowed;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.campaign-feedback__done {
|
|
223
|
+
margin: 0;
|
|
224
|
+
font-size: 0.9375rem;
|
|
225
|
+
color: var(--card-article-title);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.campaign-feedback__link {
|
|
229
|
+
margin-left: 0.5rem;
|
|
230
|
+
color: var(--key-accent, var(--primary));
|
|
231
|
+
text-decoration: underline;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
@media (max-width: 768px) {
|
|
235
|
+
.campaign-feedback {
|
|
236
|
+
margin-top: 1.75rem;
|
|
237
|
+
padding: 1rem;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* Bigger hit area: 28px stars at 4px gaps miss constantly on touch. */
|
|
241
|
+
.campaign-feedback__star {
|
|
242
|
+
font-size: 2rem;
|
|
243
|
+
padding: 0.25rem;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.campaign-feedback__submit {
|
|
247
|
+
width: 100%;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
</style>
|
package/locales/de.json
CHANGED
|
@@ -22,13 +22,54 @@
|
|
|
22
22
|
"no_requests": "Sie haben noch keine Keys angefordert"
|
|
23
23
|
},
|
|
24
24
|
"campaigns": {
|
|
25
|
+
"feedback": {
|
|
26
|
+
"title": "Dieses Spiel bewerten",
|
|
27
|
+
"subtitle": "Du hast einen Key für {game} erhalten — sag anderen Creators, was du davon hältst.",
|
|
28
|
+
"rating_label": "Deine Bewertung",
|
|
29
|
+
"title_placeholder": "In einem Satz zusammengefasst (optional)",
|
|
30
|
+
"body_placeholder": "Was funktioniert, was nicht, für wen es ist (optional)",
|
|
31
|
+
"disclosure": "Ich habe einen Key aus dieser Kampagne erhalten und diese Bewertung ist meine ehrliche Meinung.",
|
|
32
|
+
"average_note": "Deine Bewertung zählt für den öffentlichen Durchschnitt des Spiels und wird mit einem Hinweis angezeigt, dass du einen Key erhalten hast.",
|
|
33
|
+
"send": "Bewertung veröffentlichen",
|
|
34
|
+
"sending": "Wird veröffentlicht…",
|
|
35
|
+
"submitted": "Deine Bewertung ist jetzt auf der Seite von {game}.",
|
|
36
|
+
"view_on_game_page": "Dort ansehen"
|
|
37
|
+
},
|
|
38
|
+
"comments": {
|
|
39
|
+
"title": "Creator-Feedback",
|
|
40
|
+
"subtitle": "Eindrücke von Creators, die mit einem Key aus dieser Kampagne gespielt haben.",
|
|
41
|
+
"placeholder": "Erzähl, was du vom Spiel hältst…",
|
|
42
|
+
"send": "Veröffentlichen",
|
|
43
|
+
"sending": "Wird veröffentlicht…",
|
|
44
|
+
"empty": "Noch kein Feedback. Teile als Erste:r deine Eindrücke.",
|
|
45
|
+
"load_more": "Mehr laden",
|
|
46
|
+
"load_error": "Das Feedback konnte nicht geladen werden. Versuch es gleich noch einmal.",
|
|
47
|
+
"sign_in_notice": "Melde dich an, um mitzureden.",
|
|
48
|
+
"creators_only_notice": "Nur Creators, die einen Key aus dieser Kampagne erhalten haben, können hier posten.",
|
|
49
|
+
"reply": "Antworten",
|
|
50
|
+
"reply_placeholder": "Antwort schreiben…",
|
|
51
|
+
"edited": "bearbeitet",
|
|
52
|
+
"hide": "Ausblenden",
|
|
53
|
+
"unhide": "Wieder einblenden",
|
|
54
|
+
"hidden_badge": "Ausgeblendet",
|
|
55
|
+
"unknown_author": "Creator",
|
|
56
|
+
"save": "Speichern",
|
|
57
|
+
"cancel": "Abbrechen",
|
|
58
|
+
"edit": "Bearbeiten",
|
|
59
|
+
"delete": "Löschen"
|
|
60
|
+
},
|
|
25
61
|
"title": "Key Campaigns",
|
|
26
62
|
"my_requests": "My Requests",
|
|
63
|
+
"calendar_export_label": "Diese Fristen in deinen Kalender übernehmen:",
|
|
64
|
+
"calendar_export_ics": "Kalender (.ics)",
|
|
65
|
+
"calendar_export_csv": "Tabelle (.csv)",
|
|
66
|
+
"calendar_export_busy": "Wird vorbereitet...",
|
|
67
|
+
"calendar_export_error": "Die Datei konnte nicht erstellt werden. Bitte versuche es erneut.",
|
|
27
68
|
"all": "All",
|
|
28
69
|
"details": "Details",
|
|
29
70
|
"see_campaign": "See campaign",
|
|
30
71
|
"see_content": "See content",
|
|
31
|
-
"redeem": "
|
|
72
|
+
"redeem": "Request",
|
|
32
73
|
"load_more": "Load more",
|
|
33
74
|
"no_results": "No campaigns found",
|
|
34
75
|
"days": "days",
|
|
@@ -67,7 +108,7 @@
|
|
|
67
108
|
"title_span_0": "ended",
|
|
68
109
|
"obs_0": "The redemption period for the key has expired.",
|
|
69
110
|
"title_1": "Now you need to",
|
|
70
|
-
"title_span_1": "
|
|
111
|
+
"title_span_1": "request the key",
|
|
71
112
|
"obs_1": "Check the campaign requirements before requesting your key(s)",
|
|
72
113
|
"title_2": "Now just",
|
|
73
114
|
"title_span_2": "wait",
|
|
@@ -131,7 +172,7 @@
|
|
|
131
172
|
"minutes": "min",
|
|
132
173
|
"seconds": "sec",
|
|
133
174
|
"days_hours": "days and hours",
|
|
134
|
-
"redeem": "
|
|
175
|
+
"redeem": "Request key",
|
|
135
176
|
"actions": "Complementary actions",
|
|
136
177
|
"no_actions": "No actions configured",
|
|
137
178
|
"requirements": "Requirements",
|
|
@@ -195,6 +236,7 @@
|
|
|
195
236
|
"required_data": "Erforderliche Angaben",
|
|
196
237
|
"already_requested": "Sie haben bereits eine Anfrage für diese Kampagne gestellt.",
|
|
197
238
|
"sold_out": "Alle Keys für diese Kampagne wurden bereits eingelöst.",
|
|
239
|
+
"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.",
|
|
198
240
|
"keys_left": "{n} Key übrig | {n} Keys übrig",
|
|
199
241
|
"keys_low_stock": "Letzter Key | Letzte {n}",
|
|
200
242
|
"back_to_campaign": "Zur Kampagne zurückkehren",
|