@mundogamernetwork/shared-ui 1.3.1 → 1.3.3

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.
@@ -148,21 +148,21 @@ const detailRoute = computed(() =>
148
148
  <span class="lock-label">{{ $t("keys.card.exclusive_lock_title") }}</span>
149
149
  <span class="lock-cta">{{ $t("keys.card.exclusive_upgrade_cta") }}</span>
150
150
  </div>
151
- </div>
152
- </NuxtLink>
153
151
 
154
- <div class="kc-card__meta-bar">
155
- <div class="kc-card__days" v-if="card.early_access_hours && card.early_access_hours > 0 && userCanAccess">
156
- <span>{{ card.early_access_hours }}</span>h {{ $t("keys.card.days") }}
152
+ <div class="kc-card__meta-bar">
153
+ <div class="kc-card__days" v-if="card.early_access_hours && card.early_access_hours > 0 && userCanAccess">
154
+ <span>{{ card.early_access_hours }}</span>h {{ $t("keys.card.days") }}
155
+ </div>
156
+ <div class="kc-card__days" v-else>&nbsp;</div>
157
+ <span v-if="card.type" class="kc-card__type">
158
+ <svg xmlns="http://www.w3.org/2000/svg" width="12" height="13" viewBox="0 0 16 17" fill="none">
159
+ <path d="M13.3333 4.1112L12.8666 3.64453L11.9333 4.57786L10.9333 3.64453L8.33329 4.1112L2.66663 9.77786L7.19996 14.3112L12.8666 8.64453L13.3333 6.04453L12.4 5.1112L13.3333 4.1112ZM12.6 6.24453L12.2666 8.3112L7.19996 13.3779L3.59996 9.77786L8.66663 4.7112L10.7333 4.3112L11.4666 5.04453L11 5.5112L10.5333 5.04453L9.99996 5.57786L11.4 6.97786L11.8666 6.5112L11.4 6.04453L11.8666 5.57786L12.6 6.24453Z" fill="#D297FF"/>
160
+ </svg>
161
+ {{ card.type }}
162
+ </span>
163
+ </div>
157
164
  </div>
158
- <div class="kc-card__days" v-else>&nbsp;</div>
159
- <span v-if="card.type" class="kc-card__type">
160
- <svg xmlns="http://www.w3.org/2000/svg" width="12" height="13" viewBox="0 0 16 17" fill="none">
161
- <path d="M13.3333 4.1112L12.8666 3.64453L11.9333 4.57786L10.9333 3.64453L8.33329 4.1112L2.66663 9.77786L7.19996 14.3112L12.8666 8.64453L13.3333 6.04453L12.4 5.1112L13.3333 4.1112ZM12.6 6.24453L12.2666 8.3112L7.19996 13.3779L3.59996 9.77786L8.66663 4.7112L10.7333 4.3112L11.4666 5.04453L11 5.5112L10.5333 5.04453L9.99996 5.57786L11.4 6.97786L11.8666 6.5112L11.4 6.04453L11.8666 5.57786L12.6 6.24453Z" fill="#D297FF"/>
162
- </svg>
163
- {{ card.type }}
164
- </span>
165
- </div>
165
+ </NuxtLink>
166
166
  </div>
167
167
 
168
168
  <div class="kc-card__body">
@@ -317,12 +317,18 @@ const detailRoute = computed(() =>
317
317
  }
318
318
 
319
319
  &__meta-bar {
320
+ position: absolute;
321
+ left: 0;
322
+ right: 0;
323
+ bottom: 0;
320
324
  display: flex;
321
325
  align-items: center;
322
326
  justify-content: space-between;
323
327
  padding: 6px 8px;
324
- background: var(--card-bg, #181818);
328
+ background: rgba(13, 13, 13, 0.82);
329
+ backdrop-filter: blur(2px);
325
330
  min-height: 30px;
331
+ z-index: 2;
326
332
  }
327
333
 
328
334
  &__days {
package/locales/de.json CHANGED
@@ -133,7 +133,7 @@
133
133
  "rp_label": "Redeemed by",
134
134
  "who_redeem": "Who redeemed",
135
135
  "created_by": "Published by",
136
- "no_data": "Campaign not found",
136
+ "no_data": "Diese Kampagne ist noch keinem Spiel im System zugeordnet",
137
137
  "title": "Kampagne",
138
138
  "about": "Über das Spiel",
139
139
  "screenshot_alt": "Spiel-Screenshot",
@@ -237,4 +237,4 @@
237
237
  "video_thumbnail": "Video-Vorschaubild",
238
238
  "expanded_image": "Vergrößertes Bild"
239
239
  }
240
- }
240
+ }
package/locales/en.json CHANGED
@@ -133,7 +133,7 @@
133
133
  "rp_label": "Redeemed by",
134
134
  "who_redeem": "Who redeemed",
135
135
  "created_by": "Published by",
136
- "no_data": "Campaign not found",
136
+ "no_data": "This campaign is not associated with a game in our system yet",
137
137
  "title": "Campaign",
138
138
  "about": "About the game",
139
139
  "screenshot_alt": "Game screenshot",
@@ -133,7 +133,7 @@
133
133
  "rp_label": "Redeemed by",
134
134
  "who_redeem": "Who redeemed",
135
135
  "created_by": "Published by",
136
- "no_data": "Campaign not found",
136
+ "no_data": "Esta campanha ainda nao foi associada a um jogo no sistema",
137
137
  "title": "Campanha",
138
138
  "about": "Sobre o jogo",
139
139
  "screenshot_alt": "Captura de tela do jogo",
@@ -237,4 +237,4 @@
237
237
  "video_thumbnail": "Miniatura do vídeo",
238
238
  "expanded_image": "Imagem ampliada"
239
239
  }
240
- }
240
+ }
package/locales/ro.json CHANGED
@@ -133,7 +133,7 @@
133
133
  "rp_label": "Redeemed by",
134
134
  "who_redeem": "Who redeemed",
135
135
  "created_by": "Published by",
136
- "no_data": "Campaign not found",
136
+ "no_data": "Aceasta campanie nu este inca asociata cu un joc in sistem",
137
137
  "title": "Campanie",
138
138
  "about": "Despre joc",
139
139
  "screenshot_alt": "Captură de ecran a jocului",
@@ -237,4 +237,4 @@
237
237
  "video_thumbnail": "Miniatură video",
238
238
  "expanded_image": "Imagine mărită"
239
239
  }
240
- }
240
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -13,15 +13,21 @@ const routePath = route.path.split('/');
13
13
  const lang = SUPPORTED_LOCALES.find(l => routePath.includes(l)) || 'en';
14
14
 
15
15
  const config = useRuntimeConfig();
16
- // keyApiUrl overrides apiBaseURL — lets each front point to a specific API base
17
- // (e.g. community uses agency-api, not its own locale-prefixed API base).
16
+ // keyApiUrl overrides apiBaseURL — lets each front point to the API that
17
+ // registered the shared key routes.
18
18
  // Follow the same override pattern as presskit/game/[slug].vue.
19
19
  const cfg = (config.public as any)?.mgSharedUi || config.public;
20
- const apiBase: string =
20
+ const withApiV1 = (base?: string) => {
21
+ if (!base) return '';
22
+ const clean = base.replace(/\/$/, '');
23
+ return /\/api\/v1$/.test(clean) ? clean : `${clean}/api/v1`;
24
+ };
25
+ const apiBase: string = withApiV1(
21
26
  (config.public as any).keyApiUrl ||
22
27
  cfg.keyApiUrl ||
23
28
  (config.public.apiBaseURL as string) ||
24
- '';
29
+ '',
30
+ );
25
31
 
26
32
  const { data: keyData, error: fetchError, pending, refresh } = await useAsyncData(
27
33
  `public-key-${slug}`,
@@ -57,18 +63,18 @@ useSeoMeta({
57
63
  const restriction = computed(() => {
58
64
  const k = key.value;
59
65
  if (!k) return '';
60
- if (k.is_exclusive) return 'Exclusiva para streamers oficiais';
61
- if (k.is_tier_locked && k.access_tier && k.access_tier !== 'free') return `Exclusiva para o plano ${String(k.access_tier).toUpperCase()} ou superior`;
62
- if (k.streamer_type_id === 2) return 'Exclusiva para streamers oficiais';
63
- if (k.streamer_type_id === 3) return 'Exclusiva para afiliados';
66
+ if (k.is_exclusive) return 'Exclusive to official streamers';
67
+ if (k.is_tier_locked && k.access_tier && k.access_tier !== 'free') return `Exclusive to ${String(k.access_tier).toUpperCase()} plan or higher`;
68
+ if (k.streamer_type_id === 2) return 'Exclusive to official streamers';
69
+ if (k.streamer_type_id === 3) return 'Exclusive to affiliates';
64
70
  return '';
65
71
  });
66
72
  </script>
67
73
 
68
74
  <template>
69
75
  <div class="kc">
70
- <div v-if="loading" class="kc__state">Carregando…</div>
71
- <div v-else-if="error" class="kc__state">Campanha não encontrada.</div>
76
+ <div v-if="loading" class="kc__state">Loading…</div>
77
+ <div v-else-if="error" class="kc__state">Campaign not found.</div>
72
78
 
73
79
  <template v-else>
74
80
  <div class="kc__hero">
@@ -77,8 +83,8 @@ const restriction = computed(() => {
77
83
  <h1 class="kc__title">{{ key.name }}</h1>
78
84
  <div v-if="restriction" class="kc__badge">🔒 {{ restriction }}</div>
79
85
  <div class="kc__avail">
80
- <span v-if="key.available_count > 0">{{ key.available_count }} chave(s) pra resgatar</span>
81
- <span v-else>Chaves esgotadas</span>
86
+ <span v-if="key.available_count > 0">{{ key.available_count }} {{ key.available_count === 1 ? 'key' : 'keys' }} available to redeem</span>
87
+ <span v-else>Keys sold out</span>
82
88
  </div>
83
89
  </div>
84
90
  </div>
@@ -5,13 +5,13 @@
5
5
  <div class="stepper-header">
6
6
  <div
7
7
  :class="['stepper-step', { complete: currentStep > 1, active: currentStep === 1 }]"
8
- @click="currentStep = 1"
8
+ @click="setStep(1)"
9
9
  >
10
10
  {{ $t("keys.campaigns.title") }}
11
11
  </div>
12
12
  <div
13
13
  :class="['stepper-step', { complete: currentStep > 2, active: currentStep === 2 }]"
14
- @click="currentStep = 2"
14
+ @click="setStep(2)"
15
15
  >
16
16
  {{ $t("keys.campaigns.my_requests") }}
17
17
  </div>
@@ -179,23 +179,23 @@
179
179
  <span class="lock-label">{{ $t("keys.campaigns.exclusive_lock_title") }}</span>
180
180
  <span class="lock-cta">{{ $t("keys.campaigns.exclusive_upgrade_cta") }}</span>
181
181
  </div>
182
+
183
+ <div class="title">
184
+ <div class="days" v-if="card.time > 0 && card.user_can_access">
185
+ <span>{{ card.time }}</span> {{ $t("keys.campaigns.days") }}
186
+ </div>
187
+ <div class="days" v-else></div>
188
+ <span>
189
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="17" viewBox="0 0 16 17" fill="none">
190
+ <path d="M13.3333 4.1112L12.8666 3.64453L11.9333 4.57786L10.9333 3.64453L8.33329 4.1112L2.66663 9.77786L7.19996 14.3112L12.8666 8.64453L13.3333 6.04453L12.4 5.1112L13.3333 4.1112ZM12.6 6.24453L12.2666 8.3112L7.19996 13.3779L3.59996 9.77786L8.66663 4.7112L10.7333 4.3112L11.4666 5.04453L11 5.5112L10.5333 5.04453L9.99996 5.57786L11.4 6.97786L11.8666 6.5112L11.4 6.04453L11.8666 5.57786L12.6 6.24453Z" fill="var(--key-accent, var(--key-accent, var(--primary, #D297FF)))" />
191
+ </svg>
192
+ {{ card.type }}
193
+ </span>
194
+ </div>
182
195
  </div>
183
196
  </nuxt-link>
184
197
  </div>
185
198
 
186
- <div class="title">
187
- <div class="days" v-if="card.time > 0 && card.user_can_access">
188
- <span>{{ card.time }}</span> {{ $t("keys.campaigns.days") }}
189
- </div>
190
- <div class="days" v-else></div>
191
- <span>
192
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="17" viewBox="0 0 16 17" fill="none">
193
- <path d="M13.3333 4.1112L12.8666 3.64453L11.9333 4.57786L10.9333 3.64453L8.33329 4.1112L2.66663 9.77786L7.19996 14.3112L12.8666 8.64453L13.3333 6.04453L12.4 5.1112L13.3333 4.1112ZM12.6 6.24453L12.2666 8.3112L7.19996 13.3779L3.59996 9.77786L8.66663 4.7112L10.7333 4.3112L11.4666 5.04453L11 5.5112L10.5333 5.04453L9.99996 5.57786L11.4 6.97786L11.8666 6.5112L11.4 6.04453L11.8666 5.57786L12.6 6.24453Z" fill="var(--key-accent, var(--key-accent, var(--primary, #D297FF)))" />
194
- </svg>
195
- {{ card.type }}
196
- </span>
197
- </div>
198
-
199
199
  <div class="text">
200
200
  <div v-if="card.platforms && card.platforms.length" class="card-platforms">
201
201
  <span v-for="p in card.platforms" :key="p" class="card-platform-tag">
@@ -344,13 +344,15 @@
344
344
  </template>
345
345
 
346
346
  <script setup lang="ts">
347
- import { ref, computed, onMounted, onUnmounted, watch, inject } from "vue"
347
+ import { ref, computed, onMounted, onUnmounted, watch, inject, unref } from "vue"
348
348
  import { useNuxtApp, navigateTo } from "#app"
349
349
  import { useCampaignsStore } from "../../stores/campaigns"
350
350
  import { fetchMyRequests, fetchRedeemedUsers } from "../../services/campaignService"
351
+ import { useLoginStore } from "../../stores/login"
351
352
 
352
353
  // Injected by each consuming front — provide these in app.vue / a plugin
353
354
  const mgPlatform = inject<string>("mgPlatform", "MGTV")
355
+ const currentUser = inject<any>("currentUser", null)
354
356
  const loginUrl = inject<string>("loginUrl", "/")
355
357
  const upgradeRoute = inject<string>("upgradeRoute", "/")
356
358
 
@@ -358,6 +360,7 @@ const { $i18n } = useNuxtApp()
358
360
  const locale = $i18n.locale
359
361
 
360
362
  const campaignsStore = useCampaignsStore()
363
+ const loginStore = useLoginStore()
361
364
 
362
365
  const currentStep = ref(1)
363
366
  const searchTerm = ref("")
@@ -374,6 +377,26 @@ const loadingFeatured = ref(true)
374
377
  const loadingRequests = ref(false)
375
378
  const loadError = ref(false)
376
379
 
380
+ function truthyFlag(value: unknown) {
381
+ return value === true || value === 1 || value === "1"
382
+ }
383
+
384
+ function hasCurrentUser() {
385
+ return !!unref(currentUser)
386
+ }
387
+
388
+ function requestLogin() {
389
+ loginStore.showLoginModalComponent = true
390
+ }
391
+
392
+ function setStep(step: number) {
393
+ if (step === 2 && !hasCurrentUser()) {
394
+ requestLogin()
395
+ return
396
+ }
397
+ currentStep.value = step
398
+ }
399
+
377
400
  // ------------------------------------------------------------------
378
401
  // Filters
379
402
  // ------------------------------------------------------------------
@@ -489,12 +512,14 @@ async function loadFeaturedCampaigns() {
489
512
  loadingFeatured.value = true
490
513
  try {
491
514
  const response = await campaignsStore.fetchFeaturedCampaigns(mgPlatform)
492
- sliderImages.value = (response.data || []).map((item: any) => ({
493
- id: item.id,
494
- cover: item.game?.url_banner_src || item.cover_src,
495
- name: item.name,
496
- slug: item.slug,
497
- }))
515
+ sliderImages.value = (response.data || [])
516
+ .filter((item: any) => truthyFlag(item.featured) || truthyFlag(item.highlight))
517
+ .map((item: any) => ({
518
+ id: item.id,
519
+ cover: item.game?.url_banner_src || item.cover_src,
520
+ name: item.name,
521
+ slug: item.slug,
522
+ }))
498
523
  } catch {
499
524
  // silent
500
525
  } finally {
@@ -600,6 +625,7 @@ function applyUserRequestStatuses() {
600
625
  }
601
626
 
602
627
  async function loadMyRequests() {
628
+ if (!hasCurrentUser()) return
603
629
  loadingRequests.value = true
604
630
  try {
605
631
  const response = await fetchMyRequests({ page: 1, per_page: 30, sort: "id", order: "asc" })
@@ -759,7 +785,7 @@ onMounted(async () => {
759
785
  await loadCampaigns(1)
760
786
  await loadCampaignTypes()
761
787
  await loadPlatforms()
762
- await loadMyRequests()
788
+ if (hasCurrentUser()) await loadMyRequests()
763
789
 
764
790
  updateSlider()
765
791
  startAutoplay()
@@ -1141,12 +1167,18 @@ onUnmounted(() => {
1141
1167
  }
1142
1168
 
1143
1169
  .title {
1170
+ position: absolute;
1171
+ left: 0;
1172
+ right: 0;
1173
+ bottom: 0;
1144
1174
  display: flex;
1145
1175
  padding: 8px 16px;
1146
1176
  align-items: center;
1147
- background: rgba(13, 13, 13, 0.9);
1177
+ background: rgba(13, 13, 13, 0.82);
1178
+ backdrop-filter: blur(2px);
1148
1179
  width: 100%;
1149
1180
  justify-content: space-between;
1181
+ z-index: 2;
1150
1182
 
1151
1183
  .days {
1152
1184
  font-size: 12px;
@@ -147,9 +147,6 @@ async function submit() {
147
147
  return
148
148
  }
149
149
  success.value = true
150
- setTimeout(() => {
151
- navigateTo(`/${locale.value}/key-campaigns`)
152
- }, 2000)
153
150
  } catch (e: any) {
154
151
  const status = e?.response?.status
155
152
  const errorCode = e?.response?.data?.error_code
@@ -1,30 +1,66 @@
1
1
  import axios, { type AxiosInstance } from "axios"
2
2
 
3
3
  // Campaign service wraps the key-campaigns API used by TV, community, and agency.
4
- // Two clients:
5
- // mainClient public endpoints (/public/keys*), no auth required for listing
6
- // authClient authenticated endpoints (/api/v1/*), withCredentials
4
+ // Each frontend backend registers the shared key routes through its own provider.
5
+ // Authenticated calls must therefore use the current app API host, otherwise
6
+ // cookies for api.mundogamer.tv/community/agency are not sent to api-main.
7
7
 
8
8
  let _mainClient: AxiosInstance | null = null
9
9
  let _authClient: AxiosInstance | null = null
10
10
 
11
- function getMainClient(): AxiosInstance {
12
- if (_mainClient) return _mainClient
11
+ const SUPPORTED_LOCALES = ["en", "pt-BR", "es", "de", "ro", "ar-AE"]
12
+
13
+ function resolveLang(): string {
14
+ const seg =
15
+ typeof location === "undefined" ? "" : location.pathname.split("/")[1] || ""
16
+ return SUPPORTED_LOCALES.includes(seg) ? seg : "en"
17
+ }
18
+
19
+ function withApiV1(baseURL?: string): string {
20
+ if (!baseURL) return ""
21
+ const base = baseURL.replace(/\/$/, "")
22
+ return /\/api\/v1$/.test(base) ? base : `${base}/api/v1`
23
+ }
13
24
 
14
- const baseURL =
25
+ function getPublicApiBaseURL(): string {
26
+ return withApiV1(
27
+ import.meta.env.VITE_KEY_API_URL ||
15
28
  import.meta.env.VITE_CAMPAIGN_API_URL ||
29
+ import.meta.env.VITE_API_BASE_URL ||
16
30
  import.meta.env.VITE_BASE_URL_NETWORK ||
31
+ "",
32
+ )
33
+ }
34
+
35
+ function getAuthenticatedApiBaseURL(): string {
36
+ return withApiV1(
37
+ import.meta.env.VITE_KEY_AUTH_API_URL ||
38
+ import.meta.env.VITE_KEY_API_URL ||
17
39
  import.meta.env.VITE_API_BASE_URL ||
18
- ""
40
+ import.meta.env.VITE_CAMPAIGN_API_URL ||
41
+ import.meta.env.VITE_BASE_URL_NETWORK ||
42
+ "",
43
+ )
44
+ }
45
+
46
+ function setHeader(config: any, name: string, value: string) {
47
+ if (config.headers?.set) {
48
+ config.headers.set(name, value)
49
+ return
50
+ }
51
+ config.headers = { ...(config.headers || {}), [name]: value }
52
+ }
19
53
 
20
- _mainClient = axios.create({ baseURL })
54
+ function getMainClient(): AxiosInstance {
55
+ if (_mainClient) return _mainClient
56
+
57
+ _mainClient = axios.create({ baseURL: getPublicApiBaseURL() })
21
58
 
22
59
  _mainClient.interceptors.request.use((config) => {
23
- const SUPPORTED_LOCALES = ["en", "pt-BR", "es", "de", "ro", "ar-AE"]
24
- const seg =
25
- typeof location === "undefined" ? "" : location.pathname.split("/")[1] || ""
26
- const lang = SUPPORTED_LOCALES.includes(seg) ? seg : "en"
60
+ const lang = resolveLang()
61
+ const systemId = import.meta.env.VITE_SYSTEM_ID
27
62
  config.params = { ...config.params, lang }
63
+ if (systemId) setHeader(config, "X-System-Id", String(systemId))
28
64
  // Only attach the shared debug token in true local dev — "!== production"
29
65
  // also matches "hlg" (staging), which has real logged-in users relying on
30
66
  // their own session cookie via withCredentials. Attaching this static
@@ -46,20 +82,13 @@ function getMainClient(): AxiosInstance {
46
82
  function getAuthClient(): AxiosInstance {
47
83
  if (_authClient) return _authClient
48
84
 
49
- const baseURL =
50
- import.meta.env.VITE_CAMPAIGN_API_URL ||
51
- import.meta.env.VITE_BASE_URL_NETWORK ||
52
- import.meta.env.VITE_API_BASE_URL ||
53
- ""
54
-
55
- _authClient = axios.create({ baseURL, withCredentials: true })
85
+ _authClient = axios.create({ baseURL: getAuthenticatedApiBaseURL(), withCredentials: true })
56
86
 
57
87
  _authClient.interceptors.request.use((config) => {
58
- const SUPPORTED_LOCALES = ["en", "pt-BR", "es", "de", "ro", "ar-AE"]
59
- const seg =
60
- typeof location === "undefined" ? "" : location.pathname.split("/")[1] || ""
61
- const lang = SUPPORTED_LOCALES.includes(seg) ? seg : "en"
88
+ const lang = resolveLang()
89
+ const systemId = import.meta.env.VITE_SYSTEM_ID
62
90
  config.params = { ...config.params, lang }
91
+ if (systemId) setHeader(config, "X-System-Id", String(systemId))
63
92
  // Only attach the shared debug token in true local dev — "!== production"
64
93
  // also matches "hlg" (staging), which has real logged-in users relying on
65
94
  // their own session cookie via withCredentials. Attaching this static
@@ -346,7 +375,7 @@ export const fetchCampaignTypes = (params: Record<string, any> = {}) => {
346
375
  * Fetch active platforms (for the platform filter select).
347
376
  */
348
377
  export const fetchCampaignPlatforms = () => {
349
- return authClient.get("/platforms/currently")
378
+ return authClient.get("/public/key-platforms")
350
379
  }
351
380
 
352
381
  /**