@mundogamernetwork/shared-ui 1.1.67 → 1.1.69
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 +2 -2
- package/components/keys/KeyCampaignDashboardCard.vue +7 -7
- package/components/keys/KeyCampaignsCarousel.vue +190 -0
- package/locales/de.json +22 -2
- package/locales/en.json +22 -2
- package/locales/pt-BR.json +22 -2
- package/locales/ro.json +22 -2
- package/package.json +1 -1
- package/pages/key-campaign/[slug].vue +16 -1
- package/pages/key-campaigns/[slug].vue +44 -38
- package/pages/key-campaigns/index.vue +22 -22
- package/pages/key-campaigns/redeem-key-approved.vue +311 -0
- package/pages/key-campaigns/redeem-key.vue +320 -0
- package/services/campaignService.ts +12 -4
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, computed, inject, onMounted } from "vue"
|
|
3
|
+
import { useRoute, navigateTo } from "#app"
|
|
4
|
+
import { useNuxtApp } from "#app"
|
|
5
|
+
import {
|
|
6
|
+
fetchCampaignBySlug,
|
|
7
|
+
fetchKeyPlatforms,
|
|
8
|
+
fetchKeyRegions,
|
|
9
|
+
requestKey,
|
|
10
|
+
} from "../../services/campaignService"
|
|
11
|
+
|
|
12
|
+
const route = useRoute()
|
|
13
|
+
const { $i18n } = useNuxtApp()
|
|
14
|
+
const locale = $i18n.locale
|
|
15
|
+
|
|
16
|
+
const currentUser = inject<any>("currentUser", null)
|
|
17
|
+
const loginUrl = inject<string>("loginUrl", "/")
|
|
18
|
+
|
|
19
|
+
const keyId = Number(route.query.keyId)
|
|
20
|
+
|
|
21
|
+
// Redirect away if no keyId
|
|
22
|
+
if (!keyId) {
|
|
23
|
+
navigateTo(`/${locale.value}/key-campaigns`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const campaign = ref<any>(null)
|
|
27
|
+
const platforms = ref<any[]>([])
|
|
28
|
+
const regions = ref<any[]>([])
|
|
29
|
+
const loading = ref(true)
|
|
30
|
+
const submitting = ref(false)
|
|
31
|
+
const error = ref("")
|
|
32
|
+
const success = ref(false)
|
|
33
|
+
const requestError = ref("")
|
|
34
|
+
|
|
35
|
+
const selectedPlatformId = ref<number | null>(null)
|
|
36
|
+
const selectedRegionId = ref<number | null>(null)
|
|
37
|
+
const description = ref("")
|
|
38
|
+
|
|
39
|
+
onMounted(async () => {
|
|
40
|
+
if (!currentUser) {
|
|
41
|
+
const returnTo = typeof window !== "undefined" ? window.location.href : ""
|
|
42
|
+
navigateTo(`${loginUrl}?redirect_to=${encodeURIComponent(returnTo)}`, { external: true })
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const [campaignRes, platformsRes, regionsRes] = await Promise.all([
|
|
47
|
+
fetchCampaignBySlug(String(keyId)),
|
|
48
|
+
fetchKeyPlatforms(keyId),
|
|
49
|
+
fetchKeyRegions(keyId),
|
|
50
|
+
])
|
|
51
|
+
campaign.value = campaignRes?.data?.data || campaignRes?.data || null
|
|
52
|
+
platforms.value = platformsRes?.data?.data || platformsRes?.data || []
|
|
53
|
+
regions.value = regionsRes?.data?.data || regionsRes?.data || []
|
|
54
|
+
|
|
55
|
+
// Pre-select if only one option
|
|
56
|
+
if (platforms.value.length === 1) selectedPlatformId.value = platforms.value[0].id
|
|
57
|
+
if (regions.value.length === 1) selectedRegionId.value = regions.value[0].id
|
|
58
|
+
} catch {
|
|
59
|
+
error.value = $i18n.t("keys.campaigns.error_loading")
|
|
60
|
+
} finally {
|
|
61
|
+
loading.value = false
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const canSubmit = computed(() =>
|
|
66
|
+
!!selectedPlatformId.value && !!selectedRegionId.value && !submitting.value,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
async function submit() {
|
|
70
|
+
if (!canSubmit.value) return
|
|
71
|
+
submitting.value = true
|
|
72
|
+
requestError.value = ""
|
|
73
|
+
try {
|
|
74
|
+
await requestKey({
|
|
75
|
+
key_id: keyId,
|
|
76
|
+
platform_ids: [selectedPlatformId.value],
|
|
77
|
+
region_ids: [selectedRegionId.value],
|
|
78
|
+
description: description.value || "-",
|
|
79
|
+
quantity_keys: 1,
|
|
80
|
+
})
|
|
81
|
+
success.value = true
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
navigateTo(`/${locale.value}/key-campaigns`)
|
|
84
|
+
}, 2000)
|
|
85
|
+
} catch (e: any) {
|
|
86
|
+
const msg = e?.response?.data?.message || e?.message || ""
|
|
87
|
+
requestError.value = msg || $i18n.t("keys.campaigns.error_request")
|
|
88
|
+
} finally {
|
|
89
|
+
submitting.value = false
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function goBack() {
|
|
94
|
+
if (typeof window !== "undefined" && window.history.length > 1) {
|
|
95
|
+
window.history.back()
|
|
96
|
+
} else {
|
|
97
|
+
navigateTo(`/${locale.value}/key-campaigns`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<template>
|
|
103
|
+
<div id="shared-redeem-key">
|
|
104
|
+
<div class="container">
|
|
105
|
+
<div class="header">
|
|
106
|
+
<NuxtLink @click.prevent="goBack" class="back">
|
|
107
|
+
<MGIcon size="2x" icon="chevron-left" />
|
|
108
|
+
<span>{{ $t("keys.campaigns.back") }}</span>
|
|
109
|
+
</NuxtLink>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div v-if="loading" class="state">
|
|
113
|
+
<div class="skeleton" v-for="i in 3" :key="i" />
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div v-else-if="error" class="state error">{{ error }}</div>
|
|
117
|
+
|
|
118
|
+
<div v-else-if="success" class="state success">
|
|
119
|
+
<MGIcon icon="check-circle" size="3x" />
|
|
120
|
+
<p>{{ $t("keys.campaigns.request_sent") }}</p>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div v-else class="form-content">
|
|
124
|
+
<div class="title-bar">
|
|
125
|
+
<h3>{{ $t("keys.campaigns.redeem_title") }}</h3>
|
|
126
|
+
<p v-if="campaign?.name" class="campaign-name">{{ campaign.name }}</p>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<form class="form" @submit.prevent="submit">
|
|
130
|
+
<!-- Platform -->
|
|
131
|
+
<div class="field">
|
|
132
|
+
<label class="label">{{ $t("keys.campaigns.platform") }}</label>
|
|
133
|
+
<div class="options">
|
|
134
|
+
<button
|
|
135
|
+
v-for="p in platforms"
|
|
136
|
+
:key="p.id"
|
|
137
|
+
type="button"
|
|
138
|
+
class="option"
|
|
139
|
+
:class="{ selected: selectedPlatformId === p.id }"
|
|
140
|
+
@click="selectedPlatformId = p.id"
|
|
141
|
+
>
|
|
142
|
+
{{ p.name }}
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<!-- Region -->
|
|
148
|
+
<div class="field">
|
|
149
|
+
<label class="label">{{ $t("keys.campaigns.region") }}</label>
|
|
150
|
+
<div class="options">
|
|
151
|
+
<button
|
|
152
|
+
v-for="r in regions"
|
|
153
|
+
:key="r.id"
|
|
154
|
+
type="button"
|
|
155
|
+
class="option"
|
|
156
|
+
:class="{ selected: selectedRegionId === r.id }"
|
|
157
|
+
@click="selectedRegionId = r.id"
|
|
158
|
+
>
|
|
159
|
+
{{ r.name }}
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<!-- Description (optional) -->
|
|
165
|
+
<div class="field">
|
|
166
|
+
<label class="label">{{ $t("keys.campaigns.description_optional") }}</label>
|
|
167
|
+
<textarea
|
|
168
|
+
v-model="description"
|
|
169
|
+
class="textarea"
|
|
170
|
+
:placeholder="$t('keys.campaigns.description_placeholder')"
|
|
171
|
+
rows="3"
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div v-if="requestError" class="req-error">{{ requestError }}</div>
|
|
176
|
+
|
|
177
|
+
<button
|
|
178
|
+
type="submit"
|
|
179
|
+
class="btn"
|
|
180
|
+
:class="{ disabled: !canSubmit }"
|
|
181
|
+
:disabled="!canSubmit"
|
|
182
|
+
>
|
|
183
|
+
<span v-if="submitting">{{ $t("keys.campaigns.sending") }}</span>
|
|
184
|
+
<span v-else>{{ $t("keys.campaigns.campaign.redeem") }}</span>
|
|
185
|
+
</button>
|
|
186
|
+
</form>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</template>
|
|
191
|
+
|
|
192
|
+
<style lang="scss" scoped>
|
|
193
|
+
#shared-redeem-key {
|
|
194
|
+
align-items: center;
|
|
195
|
+
width: 100%;
|
|
196
|
+
justify-content: center;
|
|
197
|
+
overflow: auto;
|
|
198
|
+
height: 100%;
|
|
199
|
+
|
|
200
|
+
.container { max-width: 680px !important; padding: 32px 16px 64px; }
|
|
201
|
+
|
|
202
|
+
.header {
|
|
203
|
+
margin-bottom: 24px;
|
|
204
|
+
.back {
|
|
205
|
+
display: inline-flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
gap: 8px;
|
|
208
|
+
font-size: 14px;
|
|
209
|
+
color: var(--secondary-info-fg);
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
text-decoration: none;
|
|
212
|
+
&:hover { color: var(--card-article-title); }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.state {
|
|
217
|
+
display: flex;
|
|
218
|
+
flex-direction: column;
|
|
219
|
+
align-items: center;
|
|
220
|
+
gap: 12px;
|
|
221
|
+
padding: 64px 0;
|
|
222
|
+
text-align: center;
|
|
223
|
+
color: var(--secondary-info-fg);
|
|
224
|
+
|
|
225
|
+
&.error { color: var(--danger, #e53935); }
|
|
226
|
+
&.success { color: #4caf50; font-size: 16px; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.skeleton {
|
|
230
|
+
width: 100%;
|
|
231
|
+
height: 48px;
|
|
232
|
+
background: var(--bg-app-badge);
|
|
233
|
+
animation: pulse 1.4s ease-in-out infinite;
|
|
234
|
+
& + & { margin-top: 8px; }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.title-bar {
|
|
238
|
+
margin-bottom: 28px;
|
|
239
|
+
h3 { font-size: 20px; font-weight: 700; color: var(--card-article-title); margin: 0 0 6px; }
|
|
240
|
+
.campaign-name { font-size: 13px; color: var(--secondary-info-fg); margin: 0; }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.form { display: flex; flex-direction: column; gap: 24px; }
|
|
244
|
+
|
|
245
|
+
.field {
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-direction: column;
|
|
248
|
+
gap: 10px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.label {
|
|
252
|
+
font-size: 13px;
|
|
253
|
+
font-weight: 600;
|
|
254
|
+
color: var(--card-article-title);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.options {
|
|
258
|
+
display: flex;
|
|
259
|
+
flex-wrap: wrap;
|
|
260
|
+
gap: 8px;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.option {
|
|
264
|
+
padding: 8px 16px;
|
|
265
|
+
font-size: 13px;
|
|
266
|
+
font-weight: 500;
|
|
267
|
+
border: 1px solid var(--button-secondary-default-bg);
|
|
268
|
+
background: transparent;
|
|
269
|
+
color: var(--secondary-info-fg);
|
|
270
|
+
cursor: pointer;
|
|
271
|
+
transition: border-color 0.15s, color 0.15s;
|
|
272
|
+
|
|
273
|
+
&:hover { border-color: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF)))); color: var(--card-article-title); }
|
|
274
|
+
&.selected {
|
|
275
|
+
border-color: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF))));
|
|
276
|
+
color: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF))));
|
|
277
|
+
background: var(--key-accent-subtle-bg, rgba(210, 151, 255, 0.08));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.textarea {
|
|
282
|
+
width: 100%;
|
|
283
|
+
background: var(--bg-app-badge);
|
|
284
|
+
border: 1px solid var(--button-secondary-default-bg);
|
|
285
|
+
color: var(--card-article-title);
|
|
286
|
+
font-size: 13px;
|
|
287
|
+
padding: 10px 12px;
|
|
288
|
+
resize: vertical;
|
|
289
|
+
outline: none;
|
|
290
|
+
font-family: inherit;
|
|
291
|
+
&:focus { border-color: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF)))); }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.req-error {
|
|
295
|
+
font-size: 13px;
|
|
296
|
+
color: var(--danger, #e53935);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.btn {
|
|
300
|
+
align-self: flex-start;
|
|
301
|
+
padding: 0 28px;
|
|
302
|
+
height: 44px;
|
|
303
|
+
font-size: 14px;
|
|
304
|
+
font-weight: 700;
|
|
305
|
+
background: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF))));
|
|
306
|
+
color: #fff;
|
|
307
|
+
border: none;
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
transition: opacity 0.15s;
|
|
310
|
+
|
|
311
|
+
&.disabled, &:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
312
|
+
&:not(.disabled):hover { opacity: 0.85; }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
@keyframes pulse {
|
|
317
|
+
0%, 100% { opacity: 1; }
|
|
318
|
+
50% { opacity: 0.4; }
|
|
319
|
+
}
|
|
320
|
+
</style>
|
|
@@ -8,14 +8,21 @@ import axios, { type AxiosInstance } from "axios"
|
|
|
8
8
|
let _mainClient: AxiosInstance | null = null
|
|
9
9
|
let _authClient: AxiosInstance | null = null
|
|
10
10
|
|
|
11
|
+
// Strip /api/v1 suffix so service paths (/api/v1/...) don't double up when the
|
|
12
|
+
// env var already contains it (e.g. VITE_BASE_URL_NETWORK=https://host/api/v1).
|
|
13
|
+
function normalizeBaseUrl(url: string): string {
|
|
14
|
+
return url.replace(/\/api\/v1\/?$/, "")
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
function getMainClient(): AxiosInstance {
|
|
12
18
|
if (_mainClient) return _mainClient
|
|
13
19
|
|
|
14
|
-
const baseURL =
|
|
20
|
+
const baseURL = normalizeBaseUrl(
|
|
15
21
|
import.meta.env.VITE_CAMPAIGN_API_URL ||
|
|
16
22
|
import.meta.env.VITE_BASE_URL_NETWORK ||
|
|
17
23
|
import.meta.env.VITE_API_BASE_URL ||
|
|
18
|
-
""
|
|
24
|
+
"",
|
|
25
|
+
)
|
|
19
26
|
|
|
20
27
|
_mainClient = axios.create({ baseURL })
|
|
21
28
|
|
|
@@ -41,11 +48,12 @@ function getMainClient(): AxiosInstance {
|
|
|
41
48
|
function getAuthClient(): AxiosInstance {
|
|
42
49
|
if (_authClient) return _authClient
|
|
43
50
|
|
|
44
|
-
const baseURL =
|
|
51
|
+
const baseURL = normalizeBaseUrl(
|
|
45
52
|
import.meta.env.VITE_CAMPAIGN_API_URL ||
|
|
46
53
|
import.meta.env.VITE_BASE_URL_NETWORK ||
|
|
47
54
|
import.meta.env.VITE_API_BASE_URL ||
|
|
48
|
-
""
|
|
55
|
+
"",
|
|
56
|
+
)
|
|
49
57
|
|
|
50
58
|
_authClient = axios.create({ baseURL, withCredentials: true })
|
|
51
59
|
|