@mundogamernetwork/shared-ui 1.1.76 → 1.1.77

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
@@ -133,7 +133,9 @@
133
133
  "who_redeem": "Who redeemed",
134
134
  "created_by": "Published by",
135
135
  "no_data": "Campaign not found",
136
- "title": "Kampagne"
136
+ "title": "Kampagne",
137
+ "about": "Über das Spiel",
138
+ "screenshot_alt": "Spiel-Screenshot"
137
139
  },
138
140
  "other_campaigns": "Andere Kampagnen",
139
141
  "available": "verfügbar",
@@ -191,5 +193,9 @@
191
193
  "success_message": "Your material was sent for review. Thank you!",
192
194
  "success_close": "Close"
193
195
  }
196
+ },
197
+ "common": {
198
+ "video_thumbnail": "Video-Vorschaubild",
199
+ "expanded_image": "Vergrößertes Bild"
194
200
  }
195
201
  }
package/locales/en.json CHANGED
@@ -133,7 +133,9 @@
133
133
  "who_redeem": "Who redeemed",
134
134
  "created_by": "Published by",
135
135
  "no_data": "Campaign not found",
136
- "title": "Campaign"
136
+ "title": "Campaign",
137
+ "about": "About the game",
138
+ "screenshot_alt": "Game screenshot"
137
139
  },
138
140
  "other_campaigns": "Other campaigns",
139
141
  "available": "available",
@@ -191,5 +193,9 @@
191
193
  "success_message": "Your material was sent for review. Thank you!",
192
194
  "success_close": "Close"
193
195
  }
196
+ },
197
+ "common": {
198
+ "video_thumbnail": "Video thumbnail",
199
+ "expanded_image": "Expanded image"
194
200
  }
195
201
  }
@@ -133,7 +133,9 @@
133
133
  "who_redeem": "Who redeemed",
134
134
  "created_by": "Published by",
135
135
  "no_data": "Campaign not found",
136
- "title": "Campanha"
136
+ "title": "Campanha",
137
+ "about": "Sobre o jogo",
138
+ "screenshot_alt": "Captura de tela do jogo"
137
139
  },
138
140
  "other_campaigns": "Outras campanhas",
139
141
  "available": "disponível",
@@ -191,5 +193,9 @@
191
193
  "success_message": "Your material was sent for review. Thank you!",
192
194
  "success_close": "Close"
193
195
  }
196
+ },
197
+ "common": {
198
+ "video_thumbnail": "Miniatura do vídeo",
199
+ "expanded_image": "Imagem ampliada"
194
200
  }
195
201
  }
package/locales/ro.json CHANGED
@@ -133,7 +133,9 @@
133
133
  "who_redeem": "Who redeemed",
134
134
  "created_by": "Published by",
135
135
  "no_data": "Campaign not found",
136
- "title": "Campanie"
136
+ "title": "Campanie",
137
+ "about": "Despre joc",
138
+ "screenshot_alt": "Captură de ecran a jocului"
137
139
  },
138
140
  "other_campaigns": "Alte campanii",
139
141
  "available": "disponibil",
@@ -191,5 +193,9 @@
191
193
  "success_message": "Your material was sent for review. Thank you!",
192
194
  "success_close": "Close"
193
195
  }
196
+ },
197
+ "common": {
198
+ "video_thumbnail": "Miniatură video",
199
+ "expanded_image": "Imagine mărită"
194
200
  }
195
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.1.76",
3
+ "version": "1.1.77",
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, fetchDetailGameVideo } 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")
@@ -111,6 +112,59 @@ const { data: redeemedUsers } = await useAsyncData(
111
112
  { watch: [campaign] },
112
113
  )
113
114
 
115
+ // ------------------------------------------------------------------
116
+ // Game gallery (images/videos)
117
+ // ------------------------------------------------------------------
118
+ const { data: gameDetailsImages } = await useAsyncData(
119
+ `campaign-game-images-${slug}`,
120
+ async () => {
121
+ if (!campaign.value?.game_slug) return null
122
+ try {
123
+ const response = await fetchDetailGameImage(locale.value, campaign.value.game_slug)
124
+ return response.data.data || null
125
+ } catch {
126
+ return null
127
+ }
128
+ },
129
+ { watch: [campaign] },
130
+ )
131
+
132
+ const { data: gameDetailsVideos } = await useAsyncData(
133
+ `campaign-game-videos-${slug}`,
134
+ async () => {
135
+ if (!campaign.value?.game_slug) return null
136
+ try {
137
+ const response = await fetchDetailGameVideo(locale.value, campaign.value.game_slug)
138
+ return response.data.data || null
139
+ } catch {
140
+ return null
141
+ }
142
+ },
143
+ { watch: [campaign] },
144
+ )
145
+
146
+ const showVideoModal = ref(false)
147
+ const selectedVideoUrl = ref("")
148
+ const showImageModal = ref(false)
149
+ const selectedImageUrl = ref("")
150
+
151
+ const openVideoPlayer = (videoUrl: string) => {
152
+ selectedVideoUrl.value = videoUrl
153
+ showVideoModal.value = true
154
+ }
155
+
156
+ const openImagePreview = (imageUrl: string) => {
157
+ selectedImageUrl.value = imageUrl
158
+ showImageModal.value = true
159
+ }
160
+
161
+ const closeMediaModal = () => {
162
+ showVideoModal.value = false
163
+ showImageModal.value = false
164
+ selectedVideoUrl.value = ""
165
+ selectedImageUrl.value = ""
166
+ }
167
+
114
168
  // ------------------------------------------------------------------
115
169
  // Countdown
116
170
  // ------------------------------------------------------------------
@@ -570,6 +624,73 @@ onMounted(async () => {
570
624
  <span>{{ $t("keys.campaigns.campaign.no_data") }}</span>
571
625
  </div>
572
626
  </div>
627
+
628
+ <div class="gallery" v-if="(gameDetailsImages && gameDetailsImages.length) || (gameDetailsVideos && gameDetailsVideos.length)">
629
+ <template v-if="gameDetailsVideos && gameDetailsVideos.length > 0">
630
+ <div
631
+ v-if="gameDetailsImages"
632
+ v-for="(image, index) in gameDetailsImages.slice(0, 3)"
633
+ :key="'image-' + index"
634
+ class="image-thumbnail"
635
+ @click="openImagePreview('https:' + image.url_image_src)"
636
+ >
637
+ <img :src="'https:' + image.url_image_src" :alt="$t('keys.campaigns.campaign.screenshot_alt')" />
638
+ </div>
639
+ <div
640
+ v-for="(video, index) in gameDetailsVideos.slice(0, 1)"
641
+ :key="'video-' + index"
642
+ class="video-thumbnail"
643
+ @click="openVideoPlayer('https://www.youtube.com/embed/' + video.url_youtube)"
644
+ >
645
+ <img
646
+ :src="'https://img.youtube.com/vi/' + video.url_youtube + '/hqdefault.jpg'"
647
+ :alt="$t('common.video_thumbnail')"
648
+ />
649
+ <div class="play-icon">
650
+ <MGIcon size="2x" icon="youtube" />
651
+ </div>
652
+ </div>
653
+ </template>
654
+
655
+ <template v-else-if="gameDetailsImages && gameDetailsImages.length > 0">
656
+ <div
657
+ v-for="(image, index) in gameDetailsImages.slice(0, 4)"
658
+ :key="'image-' + index"
659
+ class="image-thumbnail"
660
+ @click="openImagePreview('https:' + image.url_image_src)"
661
+ >
662
+ <img :src="'https:' + image.url_image_src" :alt="$t('keys.campaigns.campaign.screenshot_alt')" />
663
+ </div>
664
+ </template>
665
+ </div>
666
+
667
+ <div class="media-modal" v-if="showVideoModal" @click.self="closeMediaModal">
668
+ <div class="media-modal-content">
669
+ <button class="close-button" @click="closeMediaModal">
670
+ <MGIcon size="1x" icon="close" />
671
+ </button>
672
+ <iframe
673
+ :src="selectedVideoUrl"
674
+ frameborder="0"
675
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
676
+ allowfullscreen
677
+ ></iframe>
678
+ </div>
679
+ </div>
680
+
681
+ <div class="media-modal" v-if="showImageModal" @click.self="closeMediaModal">
682
+ <div class="media-modal-content">
683
+ <button class="close-button" @click="closeMediaModal">
684
+ <MGIcon size="1x" icon="close" />
685
+ </button>
686
+ <img :src="selectedImageUrl" :alt="$t('common.expanded_image')" class="expanded-image" />
687
+ </div>
688
+ </div>
689
+
690
+ <div class="game-summary" v-if="campaign?.game_summary">
691
+ <div class="title">{{ $t("keys.campaigns.campaign.about") }}</div>
692
+ <div class="description" v-html="campaign.game_summary"></div>
693
+ </div>
573
694
  </div>
574
695
  </div>
575
696
 
@@ -932,6 +1053,71 @@ onMounted(async () => {
932
1053
  span { color: var(--card-article-title); }
933
1054
  }
934
1055
  }
1056
+
1057
+ .gallery {
1058
+ display: grid;
1059
+ grid-template-columns: repeat(4, 1fr);
1060
+ gap: 8px;
1061
+
1062
+ .image-thumbnail, .video-thumbnail {
1063
+ position: relative;
1064
+ aspect-ratio: 16 / 9;
1065
+ overflow: hidden;
1066
+ cursor: pointer;
1067
+ background-color: var(--button-secondary-default-bg);
1068
+ img { width: 100%; height: 100%; object-fit: cover; }
1069
+ }
1070
+
1071
+ .video-thumbnail {
1072
+ .play-icon {
1073
+ position: absolute;
1074
+ inset: 0;
1075
+ display: flex;
1076
+ align-items: center;
1077
+ justify-content: center;
1078
+ color: #fff;
1079
+ background: var(--overlay-dark, rgba(0, 0, 0, 0.45));
1080
+ }
1081
+ }
1082
+ }
1083
+
1084
+ .media-modal {
1085
+ position: fixed;
1086
+ inset: 0;
1087
+ z-index: 1000;
1088
+ display: flex;
1089
+ align-items: center;
1090
+ justify-content: center;
1091
+ background: var(--overlay-dark-strong, rgba(0, 0, 0, 0.75));
1092
+
1093
+ .media-modal-content {
1094
+ position: relative;
1095
+ max-width: 90vw;
1096
+ max-height: 90vh;
1097
+
1098
+ iframe { width: 80vw; height: 45vw; max-height: 80vh; border: none; }
1099
+ .expanded-image { max-width: 90vw; max-height: 90vh; object-fit: contain; }
1100
+
1101
+ .close-button {
1102
+ position: absolute;
1103
+ top: -40px;
1104
+ right: 0;
1105
+ background: transparent;
1106
+ border: none;
1107
+ color: #fff;
1108
+ cursor: pointer;
1109
+ }
1110
+ }
1111
+ }
1112
+
1113
+ .game-summary {
1114
+ display: flex;
1115
+ flex-direction: column;
1116
+ gap: 8px;
1117
+
1118
+ .title { font-size: 14px; font-weight: 600; color: var(--card-article-title); }
1119
+ .description { font-size: 13px; line-height: 1.6; color: var(--secondary-info-fg); }
1120
+ }
935
1121
  }
936
1122
  }
937
1123
 
@@ -0,0 +1,52 @@
1
+ import axios, { type AxiosInstance } from "axios"
2
+
3
+ // Game catalog service (images, videos, summary) shared across the network.
4
+ // Mirrors tv-frontend's services/games/gameDetails.ts pattern.
5
+
6
+ let _client: AxiosInstance | null = null
7
+
8
+ function getClient(): AxiosInstance {
9
+ if (_client) return _client
10
+
11
+ const baseURL =
12
+ import.meta.env.VITE_BASE_URL_COMMUNITY ||
13
+ import.meta.env.VITE_CAMPAIGN_API_URL ||
14
+ import.meta.env.VITE_BASE_URL_NETWORK ||
15
+ import.meta.env.VITE_API_BASE_URL ||
16
+ ""
17
+
18
+ _client = axios.create({ baseURL, withCredentials: true })
19
+
20
+ _client.interceptors.request.use((config) => {
21
+ if (
22
+ import.meta.env.VITE_APP_ENV !== "production" &&
23
+ typeof window !== "undefined" &&
24
+ import.meta.env.VITE_BEARER_TOKEN
25
+ ) {
26
+ config.headers.Authorization = `Bearer ${import.meta.env.VITE_BEARER_TOKEN}`
27
+ }
28
+ return config
29
+ })
30
+
31
+ return _client
32
+ }
33
+
34
+ const client = new Proxy({} as AxiosInstance, {
35
+ get(_t, prop) {
36
+ return (getClient() as any)[prop]
37
+ },
38
+ })
39
+
40
+ export interface GameImage {
41
+ url_image_src: string
42
+ }
43
+
44
+ export interface GameVideo {
45
+ url_youtube: string
46
+ }
47
+
48
+ export const fetchDetailGameImage = (lang: string, slug: string) =>
49
+ client.get<{ data: GameImage[] }>(`/${lang}/games/details/${slug}/images`)
50
+
51
+ export const fetchDetailGameVideo = (lang: string, slug: string) =>
52
+ client.get<{ data: GameVideo[] }>(`/${lang}/games/details/${slug}/videos`)