@mundogamernetwork/shared-ui 1.8.22 → 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 +41 -11
- package/components/keys/KeyCampaignCommentItem.vue +303 -0
- package/components/keys/KeyCampaignComments.vue +355 -0
- package/components/keys/KeyCampaignDashboardCard.vue +2 -2
- package/components/keys/KeyCampaignFeedback.vue +250 -0
- package/components/keys/KeyRevealModal.vue +2 -2
- 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/key-materials.vue +2 -2
- package/pages/key-campaigns/redeem-key-approved.vue +30 -24
- package/pages/key-campaigns/redeem-key.vue +58 -14
- package/pages/keys/index.vue +1 -1
- package/pages/presskit/game/[slug].vue +33 -3
- package/services/campaignService.ts +149 -2
- package/utils/embargo.ts +47 -18
- package/utils/navigation.ts +26 -0
|
@@ -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>
|
|
@@ -370,7 +370,7 @@ const detailRoute = computed(() =>
|
|
|
370
370
|
|
|
371
371
|
&__days {
|
|
372
372
|
font-size: 11px;
|
|
373
|
-
color: var(--
|
|
373
|
+
color: var(--secondary-info-fg);
|
|
374
374
|
}
|
|
375
375
|
|
|
376
376
|
&__type {
|
|
@@ -401,7 +401,7 @@ const detailRoute = computed(() =>
|
|
|
401
401
|
font-weight: 600;
|
|
402
402
|
padding: 2px 6px;
|
|
403
403
|
border: 1px solid var(--border-color, rgba(255,255,255,0.2));
|
|
404
|
-
color: var(--
|
|
404
|
+
color: var(--secondary-info-fg);
|
|
405
405
|
text-transform: uppercase;
|
|
406
406
|
letter-spacing: 0.4px;
|
|
407
407
|
}
|
|
@@ -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>
|
|
@@ -77,7 +77,7 @@ function copyCode() {
|
|
|
77
77
|
|
|
78
78
|
&__sub {
|
|
79
79
|
font-size: 0.88rem;
|
|
80
|
-
color: var(--
|
|
80
|
+
color: var(--secondary-info-fg);
|
|
81
81
|
margin: 0;
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -112,7 +112,7 @@ function copyCode() {
|
|
|
112
112
|
&__copy {
|
|
113
113
|
background: none;
|
|
114
114
|
border: none;
|
|
115
|
-
color: var(--
|
|
115
|
+
color: var(--secondary-info-fg);
|
|
116
116
|
cursor: pointer;
|
|
117
117
|
font-size: 1.1rem;
|
|
118
118
|
padding: 4px;
|