@mundogamernetwork/shared-ui 1.1.78 → 1.1.80
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/locales/de.json +6 -1
- package/locales/en.json +6 -1
- package/locales/pt-BR.json +6 -1
- package/locales/ro.json +6 -1
- package/package.json +1 -1
- package/pages/key-campaigns/[slug].vue +21 -2
- package/pages/key-campaigns/redeem-key.vue +42 -4
- package/services/campaignService.ts +0 -1
- package/services/gamesService.ts +48 -0
package/locales/de.json
CHANGED
|
@@ -169,7 +169,12 @@
|
|
|
169
169
|
},
|
|
170
170
|
"status_label": "Status",
|
|
171
171
|
"error_no_keys": "Derzeit sind keine Schlüssel für diese Kampagne verfügbar.",
|
|
172
|
-
"error_not_approved": "Ihre Anfrage wurde noch nicht genehmigt."
|
|
172
|
+
"error_not_approved": "Ihre Anfrage wurde noch nicht genehmigt.",
|
|
173
|
+
"quantity": "Anzahl der Schlüssel",
|
|
174
|
+
"error_duplicate": "Sie haben bereits eine Anfrage für diese Kampagne. Überprüfen Sie 'Meine Bestellungen' für den Status.",
|
|
175
|
+
"error_user_blocked": "Ihr Zugang zu Key-Anfragen wurde vorübergehend gesperrt, da Material-Einreichungen überfällig sind. Bitte reichen Sie Ihre ausstehenden Materialien ein.",
|
|
176
|
+
"error_not_eligible": "Diese Kampagne ist für Ihren aktuellen Plan nicht verfügbar.",
|
|
177
|
+
"error_pending_limit": "Sie haben das gleichzeitige Anfragenlimit Ihres Plans erreicht. Bitte erfüllen Sie Ihre ausstehenden Verpflichtungen, bevor Sie einen neuen Key anfordern."
|
|
173
178
|
},
|
|
174
179
|
"materials": {
|
|
175
180
|
"title": "Submit material",
|
package/locales/en.json
CHANGED
|
@@ -169,7 +169,12 @@
|
|
|
169
169
|
},
|
|
170
170
|
"status_label": "Status",
|
|
171
171
|
"error_no_keys": "No keys available for this campaign at the moment.",
|
|
172
|
-
"error_not_approved": "Your request has not been approved yet."
|
|
172
|
+
"error_not_approved": "Your request has not been approved yet.",
|
|
173
|
+
"quantity": "Number of keys",
|
|
174
|
+
"error_duplicate": "You already have a request for this campaign. Check 'My Orders' to see the status.",
|
|
175
|
+
"error_user_blocked": "Your key request access has been temporarily blocked due to overdue material submissions. Please submit your pending materials to restore access.",
|
|
176
|
+
"error_not_eligible": "This campaign is not available for your current plan.",
|
|
177
|
+
"error_pending_limit": "You have reached the simultaneous request limit for your plan. Please fulfill your pending obligations before requesting a new key."
|
|
173
178
|
},
|
|
174
179
|
"materials": {
|
|
175
180
|
"title": "Submit material",
|
package/locales/pt-BR.json
CHANGED
|
@@ -169,7 +169,12 @@
|
|
|
169
169
|
},
|
|
170
170
|
"status_label": "Status",
|
|
171
171
|
"error_no_keys": "Não há chaves disponíveis para esta campanha no momento.",
|
|
172
|
-
"error_not_approved": "Sua solicitação ainda não foi aprovada."
|
|
172
|
+
"error_not_approved": "Sua solicitação ainda não foi aprovada.",
|
|
173
|
+
"quantity": "Quantidade de chaves",
|
|
174
|
+
"error_duplicate": "Você já tem um pedido para esta campanha. Acesse 'Meus Pedidos' para ver o status.",
|
|
175
|
+
"error_user_blocked": "Seu acesso a pedidos de chave está temporariamente bloqueado por materiais não entregues. Envie seus materiais pendentes para restaurar o acesso.",
|
|
176
|
+
"error_not_eligible": "Esta campanha não está disponível para o seu plano atual.",
|
|
177
|
+
"error_pending_limit": "Você atingiu o limite de pedidos simultâneos do seu plano. Entregue o material pendente antes de solicitar uma nova chave."
|
|
173
178
|
},
|
|
174
179
|
"materials": {
|
|
175
180
|
"title": "Submit material",
|
package/locales/ro.json
CHANGED
|
@@ -169,7 +169,12 @@
|
|
|
169
169
|
},
|
|
170
170
|
"status_label": "Status",
|
|
171
171
|
"error_no_keys": "Nu există chei disponibile pentru această campanie în acest moment.",
|
|
172
|
-
"error_not_approved": "Cererea ta nu a fost aprobată încă."
|
|
172
|
+
"error_not_approved": "Cererea ta nu a fost aprobată încă.",
|
|
173
|
+
"quantity": "Numărul de chei",
|
|
174
|
+
"error_duplicate": "Aveți deja o cerere pentru această campanie. Verificați 'Comenzile mele' pentru status.",
|
|
175
|
+
"error_user_blocked": "Accesul dvs. la solicitările de chei a fost blocat temporar din cauza transmisiilor de materiale restante. Trimiteți materialele dvs. pentru a restabili accesul.",
|
|
176
|
+
"error_not_eligible": "Această campanie nu este disponibilă pentru planul dvs. actual.",
|
|
177
|
+
"error_pending_limit": "Ați atins limita de cereri simultane a planului dvs. Îndepliniți-vă obligațiile restante înainte de a solicita o nouă cheie."
|
|
173
178
|
},
|
|
174
179
|
"materials": {
|
|
175
180
|
"title": "Submit material",
|
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
revealKey,
|
|
11
11
|
requestKey,
|
|
12
12
|
} from "../../services/campaignService"
|
|
13
|
+
import { fetchDetailGameImage } from "../../services/gamesService"
|
|
13
14
|
|
|
14
15
|
// Injected by each front — provide in app.vue / a plugin
|
|
15
16
|
const mgPlatform = inject<string>("mgPlatform", "MGTV")
|
|
@@ -112,11 +113,29 @@ const { data: redeemedUsers } = await useAsyncData(
|
|
|
112
113
|
)
|
|
113
114
|
|
|
114
115
|
// ------------------------------------------------------------------
|
|
115
|
-
// Game gallery (images/videos)
|
|
116
|
+
// Game gallery (images/videos)
|
|
116
117
|
// ------------------------------------------------------------------
|
|
117
|
-
|
|
118
|
+
// Videos are already embedded in the campaign payload (YouTube IDs need no
|
|
119
|
+
// CDN prefix). Screenshots come from the dedicated games endpoint instead —
|
|
120
|
+
// the campaign payload's embedded game.images filenames aren't CDN-resolved,
|
|
121
|
+
// unlike this endpoint's response (same one tv-frontend's production
|
|
122
|
+
// gameDetails.ts already relies on).
|
|
118
123
|
const gameDetailsVideos = computed(() => campaign.value?.game_videos || [])
|
|
119
124
|
|
|
125
|
+
const { data: gameDetailsImages } = await useAsyncData(
|
|
126
|
+
`campaign-game-images-${slug}`,
|
|
127
|
+
async () => {
|
|
128
|
+
if (!campaign.value?.game_slug) return []
|
|
129
|
+
try {
|
|
130
|
+
const response = await fetchDetailGameImage(locale.value, campaign.value.game_slug)
|
|
131
|
+
return response.data.data || []
|
|
132
|
+
} catch {
|
|
133
|
+
return []
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{ watch: [campaign] },
|
|
137
|
+
)
|
|
138
|
+
|
|
120
139
|
const showVideoModal = ref(false)
|
|
121
140
|
const selectedVideoUrl = ref("")
|
|
122
141
|
const showImageModal = ref(false)
|
|
@@ -35,6 +35,15 @@ const requestError = ref("")
|
|
|
35
35
|
const selectedPlatformId = ref<number | null>(null)
|
|
36
36
|
const selectedRegionId = ref<number | null>(null)
|
|
37
37
|
const description = ref("")
|
|
38
|
+
const quantity = ref<number | null>(1)
|
|
39
|
+
|
|
40
|
+
const maxKeysForUser = computed(() => campaign.value?.max_keys_for_user ?? 1)
|
|
41
|
+
|
|
42
|
+
function clampQuantity() {
|
|
43
|
+
if (quantity.value === null) return
|
|
44
|
+
if (quantity.value < 1) quantity.value = 1
|
|
45
|
+
if (quantity.value > maxKeysForUser.value) quantity.value = maxKeysForUser.value
|
|
46
|
+
}
|
|
38
47
|
|
|
39
48
|
onMounted(async () => {
|
|
40
49
|
if (!currentUser) {
|
|
@@ -63,7 +72,7 @@ onMounted(async () => {
|
|
|
63
72
|
})
|
|
64
73
|
|
|
65
74
|
const canSubmit = computed(() =>
|
|
66
|
-
!!selectedPlatformId.value && !!selectedRegionId.value && !submitting.value,
|
|
75
|
+
!!selectedPlatformId.value && !!selectedRegionId.value && !!quantity.value && !submitting.value,
|
|
67
76
|
)
|
|
68
77
|
|
|
69
78
|
async function submit() {
|
|
@@ -76,15 +85,27 @@ async function submit() {
|
|
|
76
85
|
platform_ids: [selectedPlatformId.value],
|
|
77
86
|
region_ids: [selectedRegionId.value],
|
|
78
87
|
description: description.value || "-",
|
|
79
|
-
quantity_keys: 1,
|
|
88
|
+
quantity_keys: Number(quantity.value) || 1,
|
|
80
89
|
})
|
|
81
90
|
success.value = true
|
|
82
91
|
setTimeout(() => {
|
|
83
92
|
navigateTo(`/${locale.value}/key-campaigns`)
|
|
84
93
|
}, 2000)
|
|
85
94
|
} catch (e: any) {
|
|
86
|
-
const
|
|
87
|
-
|
|
95
|
+
const status = e?.response?.status
|
|
96
|
+
const errorCode = e?.response?.data?.error_code
|
|
97
|
+
if (status === 409) {
|
|
98
|
+
requestError.value = $i18n.t("keys.campaigns.error_duplicate")
|
|
99
|
+
} else if (status === 403 && errorCode === "user_key_blocked") {
|
|
100
|
+
requestError.value = $i18n.t("keys.campaigns.error_user_blocked")
|
|
101
|
+
} else if (status === 403) {
|
|
102
|
+
requestError.value = $i18n.t("keys.campaigns.error_not_eligible")
|
|
103
|
+
} else if (status === 422 && errorCode === "pending_limit_reached") {
|
|
104
|
+
requestError.value = $i18n.t("keys.campaigns.error_pending_limit")
|
|
105
|
+
} else {
|
|
106
|
+
const msg = e?.response?.data?.message || e?.message || ""
|
|
107
|
+
requestError.value = msg || $i18n.t("keys.campaigns.error_request")
|
|
108
|
+
}
|
|
88
109
|
} finally {
|
|
89
110
|
submitting.value = false
|
|
90
111
|
}
|
|
@@ -161,6 +182,19 @@ function goBack() {
|
|
|
161
182
|
</div>
|
|
162
183
|
</div>
|
|
163
184
|
|
|
185
|
+
<!-- Quantity -->
|
|
186
|
+
<div class="field" v-if="maxKeysForUser > 1">
|
|
187
|
+
<label class="label">{{ $t("keys.campaigns.quantity") }}</label>
|
|
188
|
+
<input
|
|
189
|
+
v-model.number="quantity"
|
|
190
|
+
type="number"
|
|
191
|
+
min="1"
|
|
192
|
+
:max="maxKeysForUser"
|
|
193
|
+
class="textarea quantity-input"
|
|
194
|
+
@change="clampQuantity"
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
164
198
|
<!-- Description (optional) -->
|
|
165
199
|
<div class="field">
|
|
166
200
|
<label class="label">{{ $t("keys.campaigns.description_optional") }}</label>
|
|
@@ -278,6 +312,10 @@ function goBack() {
|
|
|
278
312
|
}
|
|
279
313
|
}
|
|
280
314
|
|
|
315
|
+
.quantity-input {
|
|
316
|
+
width: 120px;
|
|
317
|
+
}
|
|
318
|
+
|
|
281
319
|
.textarea {
|
|
282
320
|
width: 100%;
|
|
283
321
|
background: var(--bg-app-badge);
|
|
@@ -282,7 +282,6 @@ export const fetchCampaignBySlug = async (slug: string) => {
|
|
|
282
282
|
available_at_for_user: k.available_at_for_user ?? null,
|
|
283
283
|
max_keys_for_user: k.max_keys_for_user ?? 1,
|
|
284
284
|
game_summary: k.game?.summary,
|
|
285
|
-
game_images: (k.game?.images || []) as Array<{ url_image_src: string }>,
|
|
286
285
|
game_videos: (k.game?.videos || []) as Array<{ external_id: string; name?: string }>,
|
|
287
286
|
request_id: k.requests?.[0]?.id,
|
|
288
287
|
request_item_id: k.requests?.[0]?.key_item_id,
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import axios, { type AxiosInstance } from "axios"
|
|
2
|
+
|
|
3
|
+
// Game catalog service (screenshots with CDN-resolved URLs) shared across the
|
|
4
|
+
// network. The keys/campaign API embeds raw game.images filenames without the
|
|
5
|
+
// CDN prefix, so screenshots must come from this dedicated endpoint instead
|
|
6
|
+
// (same one tv-frontend's services/games/gameDetails.ts already calls in
|
|
7
|
+
// production — mirrors that pattern rather than reinventing it).
|
|
8
|
+
|
|
9
|
+
let _client: AxiosInstance | null = null
|
|
10
|
+
|
|
11
|
+
function getClient(): AxiosInstance {
|
|
12
|
+
if (_client) return _client
|
|
13
|
+
|
|
14
|
+
const baseURL =
|
|
15
|
+
import.meta.env.VITE_BASE_URL_COMMUNITY ||
|
|
16
|
+
import.meta.env.VITE_API_BASE_URL ||
|
|
17
|
+
import.meta.env.VITE_CAMPAIGN_API_URL ||
|
|
18
|
+
import.meta.env.VITE_BASE_URL_NETWORK ||
|
|
19
|
+
""
|
|
20
|
+
|
|
21
|
+
_client = axios.create({ baseURL, withCredentials: true })
|
|
22
|
+
|
|
23
|
+
_client.interceptors.request.use((config) => {
|
|
24
|
+
if (
|
|
25
|
+
import.meta.env.VITE_APP_ENV !== "production" &&
|
|
26
|
+
typeof window !== "undefined" &&
|
|
27
|
+
import.meta.env.VITE_BEARER_TOKEN
|
|
28
|
+
) {
|
|
29
|
+
config.headers.Authorization = `Bearer ${import.meta.env.VITE_BEARER_TOKEN}`
|
|
30
|
+
}
|
|
31
|
+
return config
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
return _client
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const client = new Proxy({} as AxiosInstance, {
|
|
38
|
+
get(_t, prop) {
|
|
39
|
+
return (getClient() as any)[prop]
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
export interface GameImage {
|
|
44
|
+
url_image_src: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const fetchDetailGameImage = (lang: string, slug: string) =>
|
|
48
|
+
client.get<{ data: GameImage[] }>(`/${lang}/games/details/${slug}/images`)
|