@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.
@@ -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