@mundogamernetwork/shared-ui 1.1.82 → 1.1.83

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
@@ -135,7 +135,8 @@
135
135
  "no_data": "Campaign not found",
136
136
  "title": "Kampagne",
137
137
  "about": "Über das Spiel",
138
- "screenshot_alt": "Spiel-Screenshot"
138
+ "screenshot_alt": "Spiel-Screenshot",
139
+ "see_all": "alle anzeigen"
139
140
  },
140
141
  "other_campaigns": "Andere Kampagnen",
141
142
  "available": "verfügbar",
@@ -184,7 +185,16 @@
184
185
  "status_completed": "Anfrage abgeschlossen",
185
186
  "status_refused": "Anfrage abgelehnt",
186
187
  "status_cancelled": "Anfrage storniert",
187
- "refused_reason": "Grund der Ablehnung:"
188
+ "refused_reason": "Grund der Ablehnung:",
189
+ "good": "Super!",
190
+ "good_text": "Du hast einen Code zum Einlösen erhalten",
191
+ "approved_subtitle": "Folge den Schritten und löse deinen Schlüssel ein.",
192
+ "cody_text": "Der Code kann nur einmal eingelöst werden und läuft nach Ende des Angebots ab",
193
+ "redeem_description_1": "Öffne die Website oder App der entsprechenden Plattform (Steam, PlayStation, Xbox usw.) und melde dich bei deinem Konto an.",
194
+ "redeem_description_2": "Gehe zur Option „Code einlösen“, füge den kopierten Code ein und schließe den Vorgang ab.",
195
+ "redeem_description_3": "Der Inhalt wird auf deinem Konto aktiviert.",
196
+ "redeem_at": "Auf {platform} einlösen",
197
+ "no_redeemers": "Noch niemand hat diese Kampagne eingelöst."
188
198
  },
189
199
  "materials": {
190
200
  "title": "Submit material",
package/locales/en.json CHANGED
@@ -135,7 +135,8 @@
135
135
  "no_data": "Campaign not found",
136
136
  "title": "Campaign",
137
137
  "about": "About the game",
138
- "screenshot_alt": "Game screenshot"
138
+ "screenshot_alt": "Game screenshot",
139
+ "see_all": "see all"
139
140
  },
140
141
  "other_campaigns": "Other campaigns",
141
142
  "available": "available",
@@ -184,7 +185,16 @@
184
185
  "status_completed": "Request completed",
185
186
  "status_refused": "Request refused",
186
187
  "status_cancelled": "Request cancelled",
187
- "refused_reason": "Reason for refusal:"
188
+ "refused_reason": "Reason for refusal:",
189
+ "good": "Nice!",
190
+ "good_text": "You received a code to redeem",
191
+ "approved_subtitle": "Follow the steps and redeem your key.",
192
+ "cody_text": "The code can only be redeemed once and expires after the offer ends",
193
+ "redeem_description_1": "Open the website or app of the corresponding platform (Steam, PlayStation, Xbox, etc.) and log in to your account.",
194
+ "redeem_description_2": "Go to the 'Redeem code' option, paste the copied code, and complete the process.",
195
+ "redeem_description_3": "The content will be activated on your account.",
196
+ "redeem_at": "Redeem on {platform}",
197
+ "no_redeemers": "No one has redeemed this campaign yet."
188
198
  },
189
199
  "materials": {
190
200
  "title": "Submit material",
@@ -135,7 +135,8 @@
135
135
  "no_data": "Campaign not found",
136
136
  "title": "Campanha",
137
137
  "about": "Sobre o jogo",
138
- "screenshot_alt": "Captura de tela do jogo"
138
+ "screenshot_alt": "Captura de tela do jogo",
139
+ "see_all": "ver todos"
139
140
  },
140
141
  "other_campaigns": "Outras campanhas",
141
142
  "available": "disponível",
@@ -184,7 +185,16 @@
184
185
  "status_completed": "Solicitação concluída",
185
186
  "status_refused": "Solicitação recusada",
186
187
  "status_cancelled": "Solicitação cancelada",
187
- "refused_reason": "Motivo da recusa:"
188
+ "refused_reason": "Motivo da recusa:",
189
+ "good": "Boa!",
190
+ "good_text": "Você recebeu um código para resgatar",
191
+ "approved_subtitle": "Siga as etapas e resgate sua chave.",
192
+ "cody_text": "O código só pode ser resgatado uma vez e expira após o término da oferta",
193
+ "redeem_description_1": "Abra o site ou app da plataforma correspondente (Steam, PlayStation, Xbox, etc.) e faça login na sua conta.",
194
+ "redeem_description_2": " Vá até a opção “Resgatar código”, cole o código copiado e finalize.",
195
+ "redeem_description_3": "O conteúdo será ativado na sua conta.",
196
+ "redeem_at": "Resgatar em {platform}",
197
+ "no_redeemers": "Nenhum criador resgatou esta campanha ainda."
188
198
  },
189
199
  "materials": {
190
200
  "title": "Submit material",
package/locales/ro.json CHANGED
@@ -135,7 +135,8 @@
135
135
  "no_data": "Campaign not found",
136
136
  "title": "Campanie",
137
137
  "about": "Despre joc",
138
- "screenshot_alt": "Captură de ecran a jocului"
138
+ "screenshot_alt": "Captură de ecran a jocului",
139
+ "see_all": "vezi toate"
139
140
  },
140
141
  "other_campaigns": "Alte campanii",
141
142
  "available": "disponibil",
@@ -184,7 +185,16 @@
184
185
  "status_completed": "Cerere finalizată",
185
186
  "status_refused": "Cerere refuzată",
186
187
  "status_cancelled": "Cerere anulată",
187
- "refused_reason": "Motivul refuzului:"
188
+ "refused_reason": "Motivul refuzului:",
189
+ "good": "Super!",
190
+ "good_text": "Ai primit un cod de revendicare",
191
+ "approved_subtitle": "Urmează pașii și revendică-ți cheia.",
192
+ "cody_text": "Codul poate fi folosit o singură dată și expiră la finalul ofertei",
193
+ "redeem_description_1": "Deschide site-ul sau aplicația platformei corespunzătoare (Steam, PlayStation, Xbox etc.) și autentifică-te în contul tău.",
194
+ "redeem_description_2": "Accesează opțiunea „Redeem code”, lipește codul copiat și finalizează procesul.",
195
+ "redeem_description_3": "Conținutul va fi activat în contul tău.",
196
+ "redeem_at": "Revendică pe {platform}",
197
+ "no_redeemers": "Nimeni nu a revendicat această campanie încă."
188
198
  },
189
199
  "materials": {
190
200
  "title": "Submit material",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.1.82",
3
+ "version": "1.1.83",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -10,7 +10,7 @@ import {
10
10
  revealKey,
11
11
  requestKey,
12
12
  } from "../../services/campaignService"
13
- import { fetchDetailGameImage } from "../../services/gamesService"
13
+ import { fetchDetailGame, fetchDetailGameImage } from "../../services/gamesService"
14
14
 
15
15
  // Injected by each front — provide in app.vue / a plugin
16
16
  const mgPlatform = inject<string>("mgPlatform", "MGTV")
@@ -136,6 +136,20 @@ const { data: gameDetailsImages } = await useAsyncData(
136
136
  { watch: [campaign] },
137
137
  )
138
138
 
139
+ const { data: gameDetails } = await useAsyncData(
140
+ `campaign-game-detail-${slug}`,
141
+ async () => {
142
+ if (!campaign.value?.game_slug) return null
143
+ try {
144
+ const response = await fetchDetailGame(locale.value, campaign.value.game_slug)
145
+ return response.data.data || null
146
+ } catch {
147
+ return null
148
+ }
149
+ },
150
+ { watch: [campaign] },
151
+ )
152
+
139
153
  const showVideoModal = ref(false)
140
154
  const selectedVideoUrl = ref("")
141
155
  const showImageModal = ref(false)
@@ -600,6 +614,17 @@ onMounted(async () => {
600
614
  {{ b }}
601
615
  </div>
602
616
  </div>
617
+
618
+ <div class="tags-items" v-if="gameDetails?.platforms?.length">
619
+ <div class="tag" v-for="platform in gameDetails.platforms" :key="platform.id">
620
+ {{ platform.name }}
621
+ </div>
622
+ </div>
623
+ <div class="tags-items" v-if="gameDetails?.genres?.length">
624
+ <div class="tag" v-for="genre in gameDetails.genres" :key="genre.id">
625
+ {{ genre.localized_name }}
626
+ </div>
627
+ </div>
603
628
  </div>
604
629
 
605
630
  <!-- Cover when no game details -->
@@ -804,6 +829,36 @@ onMounted(async () => {
804
829
  <span>+{{ redeemedUsers?.total ?? 0 }}</span>
805
830
  {{ $t("keys.campaigns.creators") }}
806
831
  </div>
832
+ <div class="see" v-if="(redeemedUsers?.users?.length ?? 0) > 0" @click="showUsersModal = true">
833
+ {{ $t("keys.campaigns.campaign.see_all") }}
834
+ </div>
835
+ </div>
836
+ </div>
837
+
838
+ <div class="users-modal" v-if="showUsersModal" @click.self="showUsersModal = false">
839
+ <div class="users-modal-content">
840
+ <div class="users-modal-header">
841
+ <span>{{ $t("keys.campaigns.campaign.who_redeem") }}</span>
842
+ <button class="close-button" @click="showUsersModal = false">
843
+ <MGIcon size="1x" icon="close" />
844
+ </button>
845
+ </div>
846
+ <div class="users-modal-body">
847
+ <div
848
+ v-if="!redeemedUsers?.users?.length"
849
+ class="users-modal-empty"
850
+ >
851
+ {{ $t("keys.campaigns.no_redeemers") }}
852
+ </div>
853
+ <div v-for="(u, index) in redeemedUsers?.users" :key="index" class="users-modal-row">
854
+ <img
855
+ :src="u.avatar_url || '/imgs/avatar-default.jpg'"
856
+ :alt="u.nickname"
857
+ @error="(e: Event) => { const t = e.target as HTMLImageElement; if (t && t.src !== '/imgs/avatar-default.jpg') t.src = '/imgs/avatar-default.jpg' }"
858
+ />
859
+ <span>{{ u.nickname || `${u.first_name || ''} ${u.last_name || ''}`.trim() }}</span>
860
+ </div>
861
+ </div>
807
862
  </div>
808
863
  </div>
809
864
 
@@ -1008,6 +1063,24 @@ onMounted(async () => {
1008
1063
  i { font-size: 13px; }
1009
1064
  }
1010
1065
  }
1066
+
1067
+ .tags-items {
1068
+ display: flex;
1069
+ gap: 4px;
1070
+ flex-flow: wrap;
1071
+
1072
+ .tag {
1073
+ height: 32px;
1074
+ display: flex;
1075
+ font-size: 12px;
1076
+ color: var(--secondary-info-fg);
1077
+ font-weight: 400;
1078
+ background-color: var(--button-secondary-default-bg);
1079
+ justify-content: center;
1080
+ padding: 12px;
1081
+ align-items: center;
1082
+ }
1083
+ }
1011
1084
  }
1012
1085
 
1013
1086
  .cards {
@@ -1306,6 +1379,70 @@ onMounted(async () => {
1306
1379
  width: -webkit-fill-available;
1307
1380
  span { font-size: 16px; color: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF)))); font-weight: 600; }
1308
1381
  }
1382
+
1383
+ .see {
1384
+ font-size: 12px;
1385
+ color: var(--key-accent, var(--primary, #D297FF));
1386
+ cursor: pointer;
1387
+ text-decoration: underline;
1388
+ white-space: nowrap;
1389
+ }
1390
+ }
1391
+
1392
+ .users-modal {
1393
+ position: fixed;
1394
+ inset: 0;
1395
+ z-index: 1000;
1396
+ display: flex;
1397
+ align-items: center;
1398
+ justify-content: center;
1399
+ background: var(--overlay-dark-strong, rgba(0, 0, 0, 0.75));
1400
+
1401
+ .users-modal-content {
1402
+ width: 100%;
1403
+ max-width: 360px;
1404
+ max-height: 70vh;
1405
+ background: var(--button-secondary-default-bg);
1406
+ display: flex;
1407
+ flex-direction: column;
1408
+ }
1409
+
1410
+ .users-modal-header {
1411
+ display: flex;
1412
+ align-items: center;
1413
+ justify-content: space-between;
1414
+ padding: 16px;
1415
+ font-size: 14px;
1416
+ font-weight: 600;
1417
+ color: var(--card-article-title);
1418
+ border-bottom: 1px solid var(--bg-app-badge);
1419
+
1420
+ .close-button { background: transparent; border: none; color: var(--secondary-info-fg); cursor: pointer; }
1421
+ }
1422
+
1423
+ .users-modal-body {
1424
+ overflow-y: auto;
1425
+ padding: 8px 16px;
1426
+ }
1427
+
1428
+ .users-modal-empty {
1429
+ padding: 24px 0;
1430
+ text-align: center;
1431
+ font-size: 13px;
1432
+ color: var(--secondary-info-fg);
1433
+ }
1434
+
1435
+ .users-modal-row {
1436
+ display: flex;
1437
+ align-items: center;
1438
+ gap: 10px;
1439
+ padding: 10px 0;
1440
+ border-bottom: 1px solid var(--bg-app-badge);
1441
+ &:last-child { border-bottom: none; }
1442
+
1443
+ img { width: 36px; height: 36px; border-radius: 0; }
1444
+ span { font-size: 13px; color: var(--card-article-title); }
1445
+ }
1309
1446
  }
1310
1447
 
1311
1448
  .company {
@@ -20,6 +20,31 @@ const keyCode = ref<string | null>(null)
20
20
  const error = ref("")
21
21
  const requestData = ref<any>(null)
22
22
  const copied = ref(false)
23
+ const platform = ref("")
24
+
25
+ // Maps a platform name to its official key-activation page.
26
+ const REDEEM_URLS: Record<string, string> = {
27
+ steam: "https://store.steampowered.com/account/registerkey",
28
+ playstation: "https://store.playstation.com/redeem",
29
+ "playstation 4": "https://store.playstation.com/redeem",
30
+ "playstation 5": "https://store.playstation.com/redeem",
31
+ xbox: "https://redeem.microsoft.com",
32
+ "xbox one": "https://redeem.microsoft.com",
33
+ "xbox series x/s": "https://redeem.microsoft.com",
34
+ "pc (microsoft windows)": "https://store.steampowered.com/account/registerkey",
35
+ epic: "https://store.epicgames.com/redeem",
36
+ "epic games": "https://store.epicgames.com/redeem",
37
+ gog: "https://www.gog.com/redeem",
38
+ nintendo: "https://ecommerce.nintendo.com/redeem",
39
+ "nintendo switch": "https://ecommerce.nintendo.com/redeem",
40
+ "google stadia": "https://stadia.google.com/redeem",
41
+ }
42
+
43
+ const redeemUrl = computed(() => {
44
+ const key = (platform.value || "").trim().toLowerCase()
45
+ if (!key) return ""
46
+ return REDEEM_URLS[key] || ""
47
+ })
23
48
 
24
49
  onMounted(async () => {
25
50
  if (!currentUser) {
@@ -48,6 +73,7 @@ async function reveal() {
48
73
  const res = await revealKey(requestId)
49
74
  const data = res?.data?.data || res?.data || null
50
75
  keyCode.value = data?.key || data?.code || data?.value || null
76
+ platform.value = data?.platform || data?.platforms?.[0]?.name || ""
51
77
  if (!keyCode.value) {
52
78
  error.value = $i18n.t("keys.campaigns.error_reveal")
53
79
  }
@@ -103,49 +129,78 @@ function goBack() {
103
129
 
104
130
  <div v-else class="content">
105
131
  <div class="title-bar">
106
- <MGIcon icon="version" size="2x" class="title-icon" />
107
- <div>
108
- <h3>{{ $t("keys.campaigns.status_key.reveal_key") }}</h3>
109
- <p v-if="requestData?.key?.name" class="campaign-name">{{ requestData.key.name }}</p>
110
- </div>
132
+ <h3>
133
+ <span class="accent-text">{{ $t("keys.campaigns.good") }}</span>
134
+ {{ $t("keys.campaigns.good_text") }}
135
+ <span class="accent-text">{{ currentUser?.nickname || currentUser?.name }}</span>
136
+ </h3>
137
+ <p class="description">{{ $t("keys.campaigns.approved_subtitle") }}</p>
111
138
  </div>
112
139
 
113
- <!-- Key not yet revealed -->
114
- <div v-if="!keyCode" class="reveal-block">
115
- <p class="hint">{{ $t("keys.campaigns.reveal_hint") }}</p>
116
- <button
117
- class="btn"
118
- :class="{ disabled: revealing }"
119
- :disabled="revealing"
120
- @click="reveal"
121
- >
122
- {{ revealing ? $t("keys.campaigns.revealing") : $t("keys.campaigns.status_key.reveal_key") }}
123
- </button>
124
- <div v-if="error" class="req-error">{{ error }}</div>
125
- </div>
126
-
127
- <!-- Key revealed -->
128
- <div v-else class="key-block">
129
- <div class="key-label">{{ $t("keys.campaigns.your_key") }}</div>
130
- <div class="key-code-row">
131
- <span class="key-code">{{ keyCode }}</span>
132
- <button class="copy-btn" @click="copyCode">
133
- <MGIcon :icon="copied ? 'check' : 'copy'" size="1x" />
134
- {{ copied ? $t("keys.campaigns.copied") : $t("keys.campaigns.copy") }}
135
- </button>
140
+ <div class="cards">
141
+ <div class="side">
142
+ <div class="code-list">
143
+ <div class="code">
144
+ <div class="number">1</div>
145
+ <div class="texts">
146
+ <div class="code-box" v-if="!keyCode">
147
+ <input type="text" value="••••••••••••••••" readonly class="code-masked" />
148
+ <button class="btn" :class="{ disabled: revealing }" :disabled="revealing" @click="reveal">
149
+ {{ revealing ? $t("keys.campaigns.revealing") : $t("keys.campaigns.status_key.reveal_key") }}
150
+ </button>
151
+ </div>
152
+
153
+ <div class="code-box" v-else>
154
+ <input type="text" :value="keyCode" readonly class="code-value" />
155
+ <button class="btn" @click="copyCode">
156
+ <MGIcon :icon="copied ? 'check' : 'copy'" size="1x" />
157
+ {{ copied ? $t("keys.campaigns.copied") : $t("keys.campaigns.copy") }}
158
+ </button>
159
+ </div>
160
+
161
+ <div v-if="error" class="req-error">{{ error }}</div>
162
+
163
+ <div class="info">
164
+ <MGIcon size="1x" icon="version" />
165
+ {{ $t("keys.campaigns.cody_text") }}
166
+ </div>
167
+ </div>
168
+ </div>
169
+
170
+ <div class="code">
171
+ <div class="number">2</div>
172
+ <div class="texts">
173
+ <div class="description">{{ $t("keys.campaigns.redeem_description_1") }}</div>
174
+ <a
175
+ v-if="keyCode && redeemUrl"
176
+ :href="redeemUrl"
177
+ target="_blank"
178
+ rel="noopener"
179
+ class="btn redeem-link"
180
+ >
181
+ {{ $t("keys.campaigns.redeem_at", { platform }) }}
182
+ </a>
183
+ </div>
184
+ </div>
185
+
186
+ <div class="code">
187
+ <div class="number">3</div>
188
+ <div class="texts">
189
+ <div class="description">
190
+ {{ $t("keys.campaigns.redeem_description_2") }}
191
+ <br />
192
+ {{ $t("keys.campaigns.redeem_description_3") }}
193
+ </div>
194
+ </div>
195
+ </div>
196
+ </div>
136
197
  </div>
137
- <p class="redeem-info">{{ $t("keys.campaigns.redeem_on_platform") }}</p>
138
- </div>
139
198
 
140
- <!-- Request info -->
141
- <div v-if="requestData" class="info-grid">
142
- <div class="info-row" v-if="requestData.status_request?.name">
143
- <span class="info-label">{{ $t("keys.campaigns.status_label") }}</span>
144
- <span class="info-val">{{ requestData.status_request.name }}</span>
145
- </div>
146
- <div class="info-row" v-if="requestData.key?.name">
147
- <span class="info-label">{{ $t("keys.campaigns.campaign.title") }}</span>
148
- <span class="info-val">{{ requestData.key.name }}</span>
199
+ <div class="side-border">
200
+ <div class="title">{{ requestData?.key?.name }}</div>
201
+ <div class="imagem" v-if="requestData?.key?.cover_original_src">
202
+ <img :src="requestData.key.cover_original_src" :alt="requestData.key.name" />
203
+ </div>
149
204
  </div>
150
205
  </div>
151
206
  </div>
@@ -196,119 +251,153 @@ function goBack() {
196
251
  & + & { margin-top: 8px; }
197
252
  }
198
253
 
199
- .content { display: flex; flex-direction: column; gap: 28px; }
254
+ .content { display: flex; flex-direction: column; gap: 42px; }
200
255
 
201
256
  .title-bar {
202
- display: flex;
203
- align-items: flex-start;
204
- gap: 14px;
205
- .title-icon { color: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF)))); margin-top: 2px; }
206
- h3 { font-size: 20px; font-weight: 700; color: var(--card-article-title); margin: 0 0 4px; }
207
- .campaign-name { font-size: 13px; color: var(--secondary-info-fg); margin: 0; }
257
+ h3 {
258
+ font-size: 24px;
259
+ font-weight: 700;
260
+ color: var(--card-article-title);
261
+ margin: 0 0 8px;
262
+ }
263
+ .accent-text { color: var(--key-accent, var(--primary, #D297FF)); }
264
+ .description { font-size: 14px; color: var(--secondary-info-fg); margin: 0; }
208
265
  }
209
266
 
210
- .reveal-block {
267
+ .cards {
211
268
  display: flex;
212
- flex-direction: column;
213
- gap: 16px;
214
- padding: 24px;
215
- background: var(--bg-app-badge);
216
- border: 1px solid var(--button-secondary-default-bg);
269
+ align-items: flex-start;
270
+ gap: 84px;
271
+ width: 100%;
272
+ justify-content: space-between;
217
273
 
218
- .hint { font-size: 14px; color: var(--secondary-info-fg); margin: 0; }
274
+ @media (max-width: 767px) {
275
+ flex-direction: column;
276
+ gap: 32px;
277
+ }
219
278
  }
220
279
 
221
- .key-block {
280
+ .side {
222
281
  display: flex;
223
282
  flex-direction: column;
224
- gap: 12px;
225
- padding: 24px;
226
- background: var(--bg-app-badge);
227
- border: 1px solid var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF))));
283
+ gap: 42px;
228
284
  }
229
285
 
230
- .key-label {
231
- font-size: 12px;
232
- font-weight: 600;
233
- text-transform: uppercase;
234
- letter-spacing: 0.5px;
235
- color: var(--secondary-info-fg);
286
+ .code-list {
287
+ display: flex;
288
+ flex-direction: column;
289
+ gap: 42px;
236
290
  }
237
291
 
238
- .key-code-row {
292
+ .code {
239
293
  display: flex;
240
294
  align-items: center;
241
- gap: 12px;
242
- flex-wrap: wrap;
243
- }
295
+ gap: 24px;
296
+
297
+ .number {
298
+ display: flex;
299
+ width: 80px;
300
+ height: 80px;
301
+ padding: 24px;
302
+ flex-direction: column;
303
+ justify-content: center;
304
+ align-items: center;
305
+ background-color: var(--button-secondary-default-bg);
306
+ color: var(--key-accent, var(--primary, #D297FF));
307
+ text-align: center;
308
+ font-size: 28px;
309
+ font-weight: 700;
310
+ flex-shrink: 0;
311
+ }
244
312
 
245
- .key-code {
246
- font-size: 20px;
247
- font-weight: 700;
248
- font-family: monospace;
249
- letter-spacing: 2px;
250
- color: var(--card-article-title);
251
- word-break: break-all;
252
- }
313
+ .texts {
314
+ display: flex;
315
+ flex-direction: column;
316
+ gap: 8px;
317
+ }
253
318
 
254
- .copy-btn {
255
- display: inline-flex;
256
- align-items: center;
257
- gap: 6px;
258
- padding: 6px 14px;
259
- font-size: 13px;
260
- font-weight: 600;
261
- background: transparent;
262
- border: 1px solid var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF))));
263
- color: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF))));
264
- cursor: pointer;
265
- transition: background 0.15s;
266
- &:hover { background: var(--key-accent-subtle-bg, rgba(210, 151, 255, 0.1)); }
267
- }
319
+ .code-box {
320
+ display: flex;
321
+ align-items: center;
322
+ gap: 6px;
323
+ width: 100%;
324
+
325
+ input {
326
+ display: flex;
327
+ padding: 12px;
328
+ background-color: transparent;
329
+ height: 48px;
330
+ border: 1px solid var(--bg-app-badge);
331
+ width: 100%;
332
+ font-size: 16px;
333
+ color: var(--secondary-info-fg);
334
+ &:focus { border-color: var(--key-accent, var(--primary, #D297FF)); outline: 0; }
335
+ }
336
+
337
+ button {
338
+ background-color: var(--key-accent, var(--primary, #D297FF));
339
+ color: var(--key-accent-ink, #13161C);
340
+ height: 48px;
341
+ padding: 0 16px;
342
+ border: none;
343
+ cursor: pointer;
344
+ white-space: nowrap;
345
+ display: inline-flex;
346
+ align-items: center;
347
+ gap: 6px;
348
+ &.disabled, &:disabled { opacity: 0.45; cursor: not-allowed; }
349
+ }
350
+ }
268
351
 
269
- .redeem-info {
270
- font-size: 12px;
271
- color: var(--secondary-info-fg);
272
- margin: 0;
273
- }
352
+ .code-masked { letter-spacing: 3px; color: rgba(128, 128, 128, 0.5) !important; }
274
353
 
275
- .req-error { font-size: 13px; color: var(--danger, #e53935); }
354
+ .info {
355
+ display: flex;
356
+ align-items: flex-end;
357
+ gap: 4px;
358
+ font-size: 12px;
359
+ color: var(--secondary-info-fg);
360
+ svg { color: var(--key-accent, var(--primary, #D297FF)); }
361
+ }
276
362
 
277
- .info-grid {
278
- display: flex;
279
- flex-direction: column;
280
- gap: 8px;
281
- padding-top: 8px;
282
- border-top: 1px solid var(--button-secondary-default-bg);
363
+ .description {
364
+ font-size: 12px;
365
+ color: var(--secondary-info-fg);
366
+ }
367
+
368
+ .redeem-link {
369
+ display: inline-flex;
370
+ width: fit-content;
371
+ margin-top: 8px;
372
+ padding: 8px 14px;
373
+ background: var(--key-accent, var(--primary, #D297FF));
374
+ color: var(--key-accent-ink, #13161C);
375
+ font-size: 13px;
376
+ text-decoration: none;
377
+ }
283
378
  }
284
379
 
285
- .info-row {
380
+ .side-border {
286
381
  display: flex;
287
- gap: 12px;
288
- font-size: 13px;
289
- }
382
+ padding: 18px;
383
+ flex-direction: column;
384
+ gap: 24px;
385
+ flex: 1 0 0;
386
+ border: 1px solid var(--button-secondary-default-bg);
387
+ max-width: 391px;
290
388
 
291
- .info-label {
292
- color: var(--secondary-info-fg);
293
- min-width: 100px;
294
- }
389
+ .title { font-size: 14px; color: var(--card-article-title); font-weight: 600; }
295
390
 
296
- .info-val { color: var(--card-article-title); }
297
-
298
- .btn {
299
- align-self: flex-start;
300
- padding: 0 28px;
301
- height: 44px;
302
- font-size: 14px;
303
- font-weight: 700;
304
- background: var(--key-accent, var(--primary, var(--key-accent, var(--primary, #D297FF))));
305
- color: var(--key-accent-ink, #13161C);
306
- border: none;
307
- cursor: pointer;
308
- transition: opacity 0.15s;
309
- &.disabled, &:disabled { opacity: 0.45; cursor: not-allowed; }
310
- &:not(.disabled):hover { opacity: 0.85; }
391
+ .imagem {
392
+ width: 100%;
393
+ max-height: 200px;
394
+ overflow: hidden;
395
+ text-align: center;
396
+ img { width: auto; height: 100%; max-width: 100%; }
397
+ }
311
398
  }
399
+
400
+ .req-error { font-size: 12px; color: var(--danger, #e53935); margin-top: 4px; }
312
401
  }
313
402
 
314
403
  @keyframes pulse {
@@ -44,5 +44,13 @@ export interface GameImage {
44
44
  url_image_src: string
45
45
  }
46
46
 
47
+ export interface GameDetail {
48
+ platforms?: Array<{ id: number; name: string }>
49
+ genres?: Array<{ id: number; localized_name: string }>
50
+ }
51
+
52
+ export const fetchDetailGame = (lang: string, slug: string) =>
53
+ client.get<{ data: GameDetail }>(`/${lang}/games/details/${slug}`)
54
+
47
55
  export const fetchDetailGameImage = (lang: string, slug: string) =>
48
56
  client.get<{ data: GameImage[] }>(`/${lang}/games/details/${slug}/images`)