@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 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",
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.1.78",
3
+ "version": "1.1.80",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -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) — already embedded in the campaign payload
116
+ // Game gallery (images/videos)
116
117
  // ------------------------------------------------------------------
117
- const gameDetailsImages = computed(() => campaign.value?.game_images || [])
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 msg = e?.response?.data?.message || e?.message || ""
87
- requestError.value = msg || $i18n.t("keys.campaigns.error_request")
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`)