@mundogamernetwork/shared-ui 1.1.61 → 1.1.63

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.
@@ -0,0 +1,1162 @@
1
+ <script lang="ts" setup>
2
+ import { ref, computed, inject, onMounted } from "vue"
3
+ import { useNuxtApp, useRoute, useAsyncData, navigateTo } from "#app"
4
+ import {
5
+ fetchCampaignBySlug,
6
+ fetchRedeemedUsers,
7
+ fetchKeyActions,
8
+ fetchMyRequests,
9
+ fetchKeyRequestById,
10
+ revealKey,
11
+ requestKey,
12
+ } from "../../services/campaignService"
13
+
14
+ // Injected by each front — provide in app.vue / a plugin
15
+ const mgPlatform = inject<string>("mgPlatform", "MGTV")
16
+ const loginUrl = inject<string>("loginUrl", "/")
17
+ const upgradeRoute = inject<string>("upgradeRoute", "/")
18
+ // The consuming front should provide the authenticated user as { id, ... } or null
19
+ const currentUser = inject<any>("currentUser", null)
20
+
21
+ const { $i18n } = useNuxtApp()
22
+ const locale = $i18n.locale
23
+ const route = useRoute()
24
+ const { slug } = route.params as { slug: string }
25
+
26
+ if (!slug) {
27
+ throw createError({ statusCode: 404, statusMessage: "Campaign not found" })
28
+ }
29
+
30
+ // ------------------------------------------------------------------
31
+ // Campaign data (SSR-safe)
32
+ // ------------------------------------------------------------------
33
+ const { data: campaign, error } = await useAsyncData(`campaign-${slug}`, async () => {
34
+ try {
35
+ return await fetchCampaignBySlug(slug)
36
+ } catch (err) {
37
+ console.error("[key-campaigns/[slug]] Failed to load campaign:", err)
38
+ return null
39
+ }
40
+ })
41
+
42
+ // ------------------------------------------------------------------
43
+ // SEO meta
44
+ // ------------------------------------------------------------------
45
+ const stripHtml = (html?: string | null) =>
46
+ (html || "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim()
47
+
48
+ const absUrl = (url?: string | null) => {
49
+ if (!url) return ""
50
+ return url.startsWith("//") ? `https:${url}` : url
51
+ }
52
+
53
+ const seoTitle = () =>
54
+ campaign.value?.name
55
+ ? $i18n.t("keys.campaigns.seo.title", { game: campaign.value.name })
56
+ : $i18n.t("keys.campaigns.seo.title_generic")
57
+
58
+ const seoDescription = () =>
59
+ campaign.value?.name
60
+ ? $i18n.t("keys.campaigns.seo.description", { game: campaign.value.name })
61
+ : $i18n.t("keys.campaigns.seo.title_generic")
62
+
63
+ const seoImage = () => absUrl(campaign.value?.cover || campaign.value?.game_cover)
64
+
65
+ useSeoMeta({
66
+ title: seoTitle,
67
+ description: seoDescription,
68
+ ogTitle: seoTitle,
69
+ ogDescription: seoDescription,
70
+ ogImage: seoImage,
71
+ ogType: "website",
72
+ twitterCard: "summary_large_image",
73
+ twitterTitle: seoTitle,
74
+ twitterDescription: seoDescription,
75
+ twitterImage: seoImage,
76
+ })
77
+
78
+ // ------------------------------------------------------------------
79
+ // Key actions
80
+ // ------------------------------------------------------------------
81
+ const { data: keyActions } = await useAsyncData(
82
+ `campaign-actions-${slug}`,
83
+ async () => {
84
+ if (!campaign.value?.id) return []
85
+ try {
86
+ const response = await fetchKeyActions(campaign.value.id)
87
+ return response.data.data || []
88
+ } catch {
89
+ return []
90
+ }
91
+ },
92
+ { watch: [campaign] },
93
+ )
94
+
95
+ // ------------------------------------------------------------------
96
+ // Redeemed users
97
+ // ------------------------------------------------------------------
98
+ const { data: redeemedUsers } = await useAsyncData(
99
+ `campaign-redeemed-${slug}`,
100
+ async () => {
101
+ if (!campaign.value?.id) return { users: [], total: 0 }
102
+ try {
103
+ const response = await fetchRedeemedUsers(campaign.value.id)
104
+ const users = response.data.data || []
105
+ const total = response.data.meta?.total ?? users.length
106
+ return { users, total }
107
+ } catch {
108
+ return { users: [], total: 0 }
109
+ }
110
+ },
111
+ { watch: [campaign] },
112
+ )
113
+
114
+ // ------------------------------------------------------------------
115
+ // Countdown
116
+ // ------------------------------------------------------------------
117
+ const parseTimeToExpire = (timeString?: string): number => {
118
+ if (!timeString) return -1
119
+ const [days, hours, minutes, seconds] = timeString.split(":").map(Number)
120
+ return days * 86400 + hours * 3600 + minutes * 60 + seconds
121
+ }
122
+
123
+ const timeValues = ref({ days: 0, hours: 0, minutes: 0, seconds: 0 })
124
+
125
+ const hasNoDateLimit = computed(() => !campaign.value?.end)
126
+
127
+ const isCampaignExpired = computed(() => {
128
+ if (campaign.value?.status === false) return true
129
+ if (hasNoDateLimit.value) return false
130
+ return parseTimeToExpire(campaign.value?.time_to_expire) <= 0
131
+ })
132
+
133
+ let countdownInterval: ReturnType<typeof setInterval> | null = null
134
+
135
+ const updateCountdown = () => {
136
+ if (hasNoDateLimit.value) {
137
+ timeValues.value = { days: 999, hours: 0, minutes: 0, seconds: 0 }
138
+ return
139
+ }
140
+ if (!campaign.value?.time_to_expire) return
141
+
142
+ const [d, h, m, s] = campaign.value.time_to_expire.split(":").map(Number)
143
+ let totalSeconds = d * 86400 + h * 3600 + m * 60 + s
144
+
145
+ countdownInterval = setInterval(() => {
146
+ if (totalSeconds <= 0) {
147
+ if (countdownInterval) clearInterval(countdownInterval)
148
+ return
149
+ }
150
+ totalSeconds--
151
+ timeValues.value = {
152
+ days: Math.floor(totalSeconds / 86400),
153
+ hours: Math.floor((totalSeconds % 86400) / 3600),
154
+ minutes: Math.floor((totalSeconds % 3600) / 60),
155
+ seconds: totalSeconds % 60,
156
+ }
157
+ }, 1000)
158
+ }
159
+
160
+ const formatDate = (dateString: string | null | undefined): string => {
161
+ if (!dateString) return "-"
162
+ const date = new Date(dateString)
163
+ if (isNaN(date.getTime())) return "-"
164
+ return new Intl.DateTimeFormat(locale.value, {
165
+ hour: "2-digit",
166
+ minute: "2-digit",
167
+ day: "numeric",
168
+ month: "long",
169
+ year: "numeric",
170
+ }).format(date)
171
+ }
172
+
173
+ // ------------------------------------------------------------------
174
+ // Request state (client-side only)
175
+ // ------------------------------------------------------------------
176
+ const statusSlugToNumeric: Record<string, number> = {
177
+ pending: 2,
178
+ "under-review": 2,
179
+ approved: 3,
180
+ "waiting-material": 4,
181
+ completed: 5,
182
+ refused: 6,
183
+ cancelled: 7,
184
+ expired: 8,
185
+ }
186
+
187
+ const userRequestStatus = ref<number | null | undefined>(undefined)
188
+ const userRequestId = ref<number | null>(null)
189
+ const userKeyItemId = ref<number | null>(null)
190
+
191
+ const effectiveKeyStatus = computed(() => {
192
+ if (userRequestStatus.value !== undefined) return userRequestStatus.value
193
+ return campaign.value?.request_status
194
+ })
195
+
196
+ const effectiveRequestId = computed(() => userRequestId.value ?? campaign.value?.request_id ?? null)
197
+ const effectiveKeyItems = computed(() => userKeyItemId.value ?? campaign.value?.request_item_id ?? null)
198
+
199
+ // ------------------------------------------------------------------
200
+ // Restriction badges
201
+ // ------------------------------------------------------------------
202
+ const restrictionBadges = computed(() => {
203
+ const k = campaign.value
204
+ if (!k) return []
205
+ const badges: string[] = []
206
+ const tierLabels: Record<string, string> = { free: "Free", pro: "PRO", business: "Business", official: "Official" }
207
+
208
+ if (k.is_exclusive) {
209
+ badges.push($i18n.t("keys.campaigns.restriction.exclusive_official"))
210
+ } else if (k.is_tier_locked && k.access_tier && k.access_tier !== "free") {
211
+ badges.push($i18n.t("keys.campaigns.restriction.tier_locked", { tier: tierLabels[k.access_tier] || k.access_tier }))
212
+ } else if (k.early_access_hours && k.early_access_hours > 0 && k.access_tier && k.access_tier !== "free") {
213
+ badges.push($i18n.t("keys.campaigns.restriction.early_access", { tier: tierLabels[k.access_tier] || k.access_tier }))
214
+ }
215
+ if (k.streamer_type === 2) {
216
+ badges.push($i18n.t("keys.campaigns.restriction.level_official"))
217
+ } else if (k.streamer_type === 3) {
218
+ badges.push($i18n.t("keys.campaigns.restriction.level_affiliate"))
219
+ }
220
+ return badges
221
+ })
222
+
223
+ // ------------------------------------------------------------------
224
+ // UserRequestsValidation inline state (ported from TV component)
225
+ // ------------------------------------------------------------------
226
+ type StatusConfig = {
227
+ titleParts: { text: string; color?: "default" | "yellow" | "red" }[]
228
+ obs: string
229
+ obsColor?: "default" | "yellow" | "red"
230
+ progress?: { percent: number; step: number; total: number; color?: "default" | "yellow" | "complete" }
231
+ actions?: Array<{ type: string; disabled?: boolean }>
232
+ canViewCode?: boolean
233
+ }
234
+
235
+ const statusConfigs: Record<string, StatusConfig> = {
236
+ expired: {
237
+ titleParts: [
238
+ { text: "keys.campaigns.status_key.title_0" },
239
+ { text: "keys.campaigns.status_key.title_span_0", color: "red" },
240
+ ],
241
+ obs: "keys.campaigns.status_key.obs_0",
242
+ obsColor: "red",
243
+ },
244
+ exhausted: {
245
+ titleParts: [
246
+ { text: "keys.campaigns.status_key.title_exhausted" },
247
+ { text: "keys.campaigns.status_key.title_span_exhausted", color: "red" },
248
+ ],
249
+ obs: "keys.campaigns.status_key.obs_exhausted",
250
+ obsColor: "red",
251
+ },
252
+ locked_exclusive: {
253
+ titleParts: [
254
+ { text: "keys.campaigns.status_key.title_locked" },
255
+ { text: "keys.campaigns.status_key.title_span_locked", color: "yellow" },
256
+ ],
257
+ obs: "keys.campaigns.status_key.obs_locked_exclusive",
258
+ obsColor: "yellow",
259
+ actions: [{ type: "upgrade" }],
260
+ },
261
+ locked_early: {
262
+ titleParts: [
263
+ { text: "keys.campaigns.status_key.title_locked" },
264
+ { text: "keys.campaigns.status_key.title_span_locked", color: "yellow" },
265
+ ],
266
+ obs: "keys.campaigns.status_key.obs_locked_early",
267
+ obsColor: "yellow",
268
+ actions: [{ type: "upgrade" }],
269
+ },
270
+ locked_tier: {
271
+ titleParts: [
272
+ { text: "keys.campaigns.status_key.title_locked" },
273
+ { text: "keys.campaigns.status_key.title_span_locked", color: "yellow" },
274
+ ],
275
+ obs: "keys.campaigns.status_key.obs_locked_tier",
276
+ obsColor: "yellow",
277
+ actions: [{ type: "upgrade" }],
278
+ },
279
+ locked_official: {
280
+ titleParts: [
281
+ { text: "keys.campaigns.status_key.title_level" },
282
+ { text: "keys.campaigns.status_key.title_span_level_official", color: "yellow" },
283
+ ],
284
+ obs: "keys.campaigns.status_key.obs_level_official",
285
+ obsColor: "yellow",
286
+ },
287
+ locked_affiliate: {
288
+ titleParts: [
289
+ { text: "keys.campaigns.status_key.title_level" },
290
+ { text: "keys.campaigns.status_key.title_span_level_affiliate", color: "yellow" },
291
+ ],
292
+ obs: "keys.campaigns.status_key.obs_level_affiliate",
293
+ obsColor: "yellow",
294
+ },
295
+ "1": {
296
+ titleParts: [
297
+ { text: "keys.campaigns.status_key.title_1" },
298
+ { text: "keys.campaigns.status_key.title_span_1" },
299
+ ],
300
+ obs: "keys.campaigns.status_key.obs_1",
301
+ progress: { percent: 25, step: 1, total: 4 },
302
+ actions: [{ type: "redeem", disabled: false }],
303
+ },
304
+ "2": {
305
+ titleParts: [
306
+ { text: "keys.campaigns.status_key.title_2" },
307
+ { text: "keys.campaigns.status_key.title_span_2", color: "yellow" },
308
+ ],
309
+ obs: "keys.campaigns.status_key.obs_2",
310
+ obsColor: "yellow",
311
+ progress: { percent: 50, step: 2, total: 4, color: "yellow" },
312
+ },
313
+ "3": {
314
+ titleParts: [
315
+ { text: "keys.campaigns.status_key.title_approved" },
316
+ { text: "keys.campaigns.status_key.title_span_approved" },
317
+ ],
318
+ obs: "keys.campaigns.status_key.obs_approved",
319
+ progress: { percent: 50, step: 2, total: 4 },
320
+ actions: [{ type: "reveal-key" }],
321
+ },
322
+ "4": {
323
+ titleParts: [
324
+ { text: "keys.campaigns.status_key.title_3" },
325
+ { text: "keys.campaigns.status_key.title_span_3" },
326
+ { text: "keys.campaigns.status_key.title_3_2" },
327
+ ],
328
+ obs: "keys.campaigns.status_key.obs_3",
329
+ progress: { percent: 75, step: 3, total: 4 },
330
+ actions: [{ type: "send-materials" }],
331
+ canViewCode: true,
332
+ },
333
+ "5": {
334
+ titleParts: [
335
+ { text: "keys.campaigns.status_key.title_span_4" },
336
+ { text: "keys.campaigns.status_key.title_4" },
337
+ ],
338
+ obs: "keys.campaigns.status_key.obs_4",
339
+ progress: { percent: 100, step: 4, total: 4, color: "complete" },
340
+ actions: [{ type: "complete-materials" }],
341
+ canViewCode: true,
342
+ },
343
+ "6": {
344
+ titleParts: [
345
+ { text: "keys.campaigns.status_key.title_5" },
346
+ { text: "keys.campaigns.status_key.title_span_5", color: "red" },
347
+ ],
348
+ obs: "keys.campaigns.status_key.obs_5",
349
+ obsColor: "red",
350
+ },
351
+ "7": {
352
+ titleParts: [
353
+ { text: "keys.campaigns.status_key.title_6" },
354
+ { text: "keys.campaigns.status_key.title_span_6", color: "red" },
355
+ ],
356
+ obs: "keys.campaigns.status_key.obs_6",
357
+ obsColor: "red",
358
+ },
359
+ "8": {
360
+ titleParts: [
361
+ { text: "keys.campaigns.status_key.title_7" },
362
+ { text: "keys.campaigns.status_key.title_span_7", color: "red" },
363
+ ],
364
+ obs: "keys.campaigns.status_key.obs_7",
365
+ obsColor: "red",
366
+ },
367
+ "9": {
368
+ titleParts: [
369
+ { text: "keys.campaigns.status_key.title_8" },
370
+ { text: "keys.campaigns.status_key.title_span_8", color: "red" },
371
+ ],
372
+ obs: "keys.campaigns.status_key.obs_8",
373
+ obsColor: "red",
374
+ },
375
+ }
376
+
377
+ const currentConfig = computed((): StatusConfig | null => {
378
+ const status = effectiveKeyStatus.value
379
+ const campaignExpired = !hasNoDateLimit.value && parseTimeToExpire(campaign.value?.time_to_expire) <= 0
380
+ const campaignExhausted = (campaign.value?.available_count ?? 0) < 1
381
+ const noRequestYet = status === null || status === undefined || status === 1
382
+ const userCanAccess = campaign.value?.user_can_access !== false
383
+
384
+ if (noRequestYet && !campaignExpired && !userCanAccess) {
385
+ if (campaign.value?.is_exclusive) return statusConfigs.locked_exclusive
386
+ if (campaign.value?.is_tier_locked) return statusConfigs.locked_tier
387
+ return statusConfigs.locked_early
388
+ }
389
+
390
+ if (status === null || status === undefined) {
391
+ if (campaignExpired) return statusConfigs.expired
392
+ if (campaignExhausted) return statusConfigs.exhausted
393
+ return statusConfigs["1"]
394
+ }
395
+
396
+ if (status === 1 && campaignExpired) return statusConfigs.expired
397
+ if (status === 1 && campaignExhausted) return statusConfigs.exhausted
398
+ if (status === 1) return statusConfigs["1"]
399
+
400
+ return statusConfigs[String(status)] || null
401
+ })
402
+
403
+ const redeemDisabled = computed(() => {
404
+ const action = currentConfig.value?.actions?.[0]
405
+ if (!action || action.type !== "redeem") return true
406
+ return action.disabled || (campaign.value?.available_count ?? 0) < 1
407
+ })
408
+
409
+ const tierLabel = computed(() => {
410
+ const labels: Record<string, string> = { free: "Free", pro: "PRO", business: "Business", official: "Official" }
411
+ return labels[campaign.value?.access_tier || "free"] || (campaign.value?.access_tier || "free")
412
+ })
413
+
414
+ // ------------------------------------------------------------------
415
+ // Auth guard for CTA actions
416
+ // ------------------------------------------------------------------
417
+ function requireAuth(targetPath: string) {
418
+ if (currentUser) {
419
+ navigateTo(targetPath)
420
+ return
421
+ }
422
+ const returnTo = typeof window !== "undefined"
423
+ ? `${window.location.origin}${route.fullPath}`
424
+ : targetPath
425
+ navigateTo(`${loginUrl}?redirect_to=${encodeURIComponent(returnTo)}`, { external: true })
426
+ }
427
+
428
+ // ------------------------------------------------------------------
429
+ // Modal state (redeemed users list)
430
+ // ------------------------------------------------------------------
431
+ const showUsersModal = ref(false)
432
+
433
+ // ------------------------------------------------------------------
434
+ // Lifecycle
435
+ // ------------------------------------------------------------------
436
+ onMounted(async () => {
437
+ updateCountdown()
438
+
439
+ if (currentUser && campaign.value?.id) {
440
+ try {
441
+ const response = await fetchMyRequests({ "filter[key_id]": campaign.value.id })
442
+ const requests = response?.data?.data
443
+ if (requests && requests.length > 0) {
444
+ const slugStatus = requests[0]?.status_request?.slug
445
+ userRequestStatus.value = slugStatus ? (statusSlugToNumeric[slugStatus] ?? null) : null
446
+ userRequestId.value = requests[0]?.id ?? null
447
+ userKeyItemId.value = requests[0]?.key_item_id ?? null
448
+ } else {
449
+ userRequestStatus.value = null
450
+ }
451
+ } catch {
452
+ userRequestStatus.value = null
453
+ }
454
+ }
455
+ })
456
+ </script>
457
+
458
+ <template>
459
+ <div id="shared-campaign-detail">
460
+ <div class="container">
461
+ <div class="header">
462
+ <NuxtLink class="back" :to="`/${locale}/key-campaigns`">
463
+ <MGIcon size="2x" icon="chevron-left" />
464
+ <span>{{ $t("keys.campaigns.back") }}</span>
465
+ </NuxtLink>
466
+
467
+ <!-- Inline UserRequestsValidation -->
468
+ <div class="rescue" v-if="currentConfig">
469
+ <div class="rescue-texts">
470
+ <div class="title">
471
+ <template v-for="(part, i) in currentConfig.titleParts" :key="i">
472
+ <template v-if="i > 0"> </template>
473
+ <span v-if="part.color" :class="part.color">{{ $t(part.text) }}</span>
474
+ <template v-if="!part.color">{{ $t(part.text) }}</template>
475
+ </template>
476
+ </div>
477
+
478
+ <div class="bar" v-if="currentConfig.progress">
479
+ <div
480
+ class="progress"
481
+ :class="currentConfig.progress.color || ''"
482
+ :style="{ width: currentConfig.progress.percent + '%' }"
483
+ >
484
+ <span>
485
+ {{ currentConfig.progress.step }}
486
+ {{ $t("keys.campaigns.status_key.of") }}
487
+ {{ currentConfig.progress.total }}
488
+ </span>
489
+ </div>
490
+ </div>
491
+
492
+ <div class="info">
493
+ <MGIcon size="1x" icon="version" :class="currentConfig.obsColor || ''" />
494
+ {{ $t(currentConfig.obs, { tier: tierLabel }) }}
495
+ </div>
496
+ </div>
497
+
498
+ <div class="rescue-actions" v-if="currentConfig.actions?.length || (currentConfig.canViewCode && effectiveRequestId)">
499
+ <template v-if="currentConfig.actions?.[0]?.type === 'upgrade'">
500
+ <button class="btn" @click="requireAuth(upgradeRoute)">
501
+ {{ $t("keys.campaigns.upgrade_plan") }}
502
+ </button>
503
+ </template>
504
+
505
+ <template v-else-if="currentConfig.actions?.[0]?.type === 'redeem'">
506
+ <button
507
+ class="btn"
508
+ :class="{ disabled: redeemDisabled }"
509
+ :disabled="redeemDisabled"
510
+ @click="requireAuth(`/${locale}/key-campaigns/redeem-key?keyId=${campaign?.id}`)"
511
+ >
512
+ {{ $t("keys.campaigns.campaign.redeem") }}
513
+ </button>
514
+ </template>
515
+
516
+ <template v-else-if="currentConfig.actions?.[0]?.type === 'reveal-key'">
517
+ <button class="btn" @click="requireAuth(`/${locale}/key-campaigns/redeem-key-approved?requestId=${effectiveRequestId}${effectiveKeyItems ? '&keyItems=' + effectiveKeyItems : ''}`)">
518
+ {{ $t("keys.campaigns.status_key.reveal_key") }}
519
+ </button>
520
+ </template>
521
+
522
+ <template v-else-if="currentConfig.actions?.[0]?.type === 'send-materials'">
523
+ <button class="btn" @click="requireAuth(`/${locale}/key-campaigns/key-materials?requestId=${effectiveRequestId}`)">
524
+ {{ $t("keys.materials.title") }}
525
+ </button>
526
+ </template>
527
+
528
+ <template v-else-if="currentConfig.actions?.[0]?.type === 'complete-materials'">
529
+ <button class="btn" @click="requireAuth(`/${locale}/key-campaigns/key-materials?requestId=${effectiveRequestId}`)">
530
+ {{ $t("keys.campaigns.status_key.complete") }}
531
+ </button>
532
+ </template>
533
+
534
+ <a
535
+ v-if="currentConfig.canViewCode && effectiveRequestId"
536
+ class="view-code-link"
537
+ @click.prevent="requireAuth(`/${locale}/key-campaigns/redeem-key-approved?requestId=${effectiveRequestId}${effectiveKeyItems ? '&keyItems=' + effectiveKeyItems : ''}`)"
538
+ >
539
+ <MGIcon size="1x" icon="version" />
540
+ {{ $t("keys.campaigns.status_key.view_code") }}
541
+ </a>
542
+ </div>
543
+ </div>
544
+ </div>
545
+
546
+ <div class="content-key">
547
+ <div class="content-key-header">
548
+ <h4>{{ campaign?.name }}</h4>
549
+
550
+ <div class="restriction-badges" v-if="restrictionBadges.length">
551
+ <div class="restriction-badge" v-for="(b, i) in restrictionBadges" :key="i">
552
+ <MGIcon size="1x" icon="lock" />
553
+ {{ b }}
554
+ </div>
555
+ </div>
556
+ </div>
557
+
558
+ <!-- Cover when no game details -->
559
+ <div class="featured-cover" v-if="campaign?.cover">
560
+ <img :src="campaign.cover" :alt="campaign.name" />
561
+ </div>
562
+
563
+ <div class="cards">
564
+ <div class="side">
565
+ <div class="about">
566
+ <div class="game-cover">
567
+ <img :src="campaign?.game_cover || '/imgs/no-cover-img.jpg'" alt="Cover" />
568
+ <div class="info">
569
+ {{ $t("keys.campaigns.campaign.launch") }}
570
+ <span>{{ $t("keys.campaigns.campaign.no_data") }}</span>
571
+ </div>
572
+ </div>
573
+ </div>
574
+ </div>
575
+
576
+ <div class="side-border">
577
+ <div class="status">
578
+ {{ $t("keys.campaigns.campaign.status") }}
579
+ <div class="tags">
580
+ <div class="tag">
581
+ <span :class="{
582
+ active: !isCampaignExpired && (campaign?.available_count ?? 0) > 0 && (hasNoDateLimit || timeValues.days > 7),
583
+ ending: !isCampaignExpired && (campaign?.available_count ?? 0) > 0 && !hasNoDateLimit && timeValues.days <= 7 && timeValues.days > 0,
584
+ inactive: isCampaignExpired || (campaign?.available_count ?? 0) === 0,
585
+ }">●</span>
586
+ {{
587
+ isCampaignExpired
588
+ ? $t("keys.campaigns.campaign.status_closed")
589
+ : (campaign?.available_count ?? 0) === 0
590
+ ? $t("keys.campaigns.campaign.status_exhausted")
591
+ : hasNoDateLimit
592
+ ? $t("keys.campaigns.campaign.status_active")
593
+ : timeValues.days <= 7
594
+ ? $t("keys.campaigns.campaign.status_ending")
595
+ : $t("keys.campaigns.campaign.status_active")
596
+ }}
597
+ </div>
598
+ <div class="tag-quantity" v-if="!isCampaignExpired">
599
+ <span :class="{
600
+ active: (campaign?.available_count ?? 0) > 0 && (hasNoDateLimit || timeValues.days > 7),
601
+ ending: (campaign?.available_count ?? 0) > 0 && !hasNoDateLimit && timeValues.days <= 7,
602
+ inactive: (campaign?.available_count ?? 0) === 0,
603
+ }">{{ campaign?.available_count || 0 }} {{ $t("keys.campaigns.campaign.keys") }}</span>
604
+ {{ $t("keys.campaigns.campaign.available") }}
605
+ </div>
606
+ </div>
607
+ </div>
608
+
609
+ <div class="time" v-if="!hasNoDateLimit">
610
+ <div class="card-time">
611
+ {{ timeValues.days }}<span>{{ $t("keys.campaigns.campaign.days") }}</span>
612
+ </div>
613
+ <div class="card-time">
614
+ {{ timeValues.hours }}<span>{{ $t("keys.campaigns.campaign.hours") }}</span>
615
+ </div>
616
+ <div class="card-time">
617
+ {{ timeValues.minutes }}<span>{{ $t("keys.campaigns.campaign.minutes") }}</span>
618
+ </div>
619
+ <div class="card-time">
620
+ {{ timeValues.seconds }}<span>{{ $t("keys.campaigns.campaign.seconds") }}</span>
621
+ </div>
622
+ </div>
623
+
624
+ <div class="requirements">
625
+ {{ $t("keys.campaigns.campaign.requirements") }}
626
+ <div class="tags">
627
+ <div class="tag" :class="{ active: campaign?.streamer_type === 1 }">
628
+ <MGIcon icon="community" /> {{ $t("keys.campaigns.campaign.requirement.all") }}
629
+ </div>
630
+ <div class="tag" :class="{ active: campaign?.streamer_type === 2 || campaign?.streamer_type === 1 }">
631
+ <MGIcon icon="verified" /> {{ $t("keys.campaigns.campaign.requirement.official") }}
632
+ </div>
633
+ <div class="tag" :class="{ active: campaign?.streamer_type === 3 || campaign?.streamer_type === 1 }">
634
+ <MGIcon icon="partner" /> {{ $t("keys.campaigns.campaign.requirement.affiliate") }}
635
+ </div>
636
+ </div>
637
+ <div class="tags" v-if="campaign?.streamer_description">
638
+ <div class="tag-bg">{{ campaign.streamer_description }}</div>
639
+ </div>
640
+ </div>
641
+
642
+ <div class="actions">
643
+ {{ $t("keys.campaigns.campaign.actions") }}
644
+ <template v-if="keyActions && keyActions.length > 0">
645
+ <div class="tag" v-for="action in keyActions" :key="action.id">
646
+ <div class="icon">
647
+ <MGIcon :icon="action.action_type === 'live' ? 'live' : action.action_type === 'tweet' ? 'twitter' : action.action_type === 'post' ? 'instagram' : action.action_type === 'review' ? 'star' : 'check'" />
648
+ </div>
649
+ <div class="texts">
650
+ {{ action.title }}
651
+ <div class="small" v-if="action.description">{{ action.description }}</div>
652
+ <div class="small reward">
653
+ <span>{{ action.reward_type === "mgc" ? "MGC" : "R$" }} {{ action.reward_amount }}</span>
654
+ </div>
655
+ </div>
656
+ </div>
657
+ </template>
658
+ <div class="empty-actions" v-else>
659
+ {{ $t("keys.campaigns.campaign.no_actions") }}
660
+ </div>
661
+ </div>
662
+
663
+ <div class="actions">
664
+ {{ $t("keys.campaigns.campaign.days_hours") }}
665
+ <div class="data">
666
+ <div class="info">
667
+ {{ $t("keys.campaigns.campaign.start") }}
668
+ <span>{{ formatDate(campaign?.start) }}</span>
669
+ </div>
670
+ <div class="info">
671
+ {{ $t("keys.campaigns.campaign.end") }}
672
+ <span>{{ formatDate(campaign?.end) }}</span>
673
+ </div>
674
+ </div>
675
+ </div>
676
+
677
+ <div class="actions">
678
+ {{ $t("keys.campaigns.campaign.who_redeem") }}
679
+ <div class="users">
680
+ <div class="imgs">
681
+ <img
682
+ v-for="(u, index) in redeemedUsers?.users?.slice(0, 4)"
683
+ :key="index"
684
+ :src="u.avatar_url"
685
+ :alt="u.nickname"
686
+ />
687
+ </div>
688
+ <div class="text">
689
+ <span>+{{ redeemedUsers?.total ?? 0 }}</span>
690
+ {{ $t("keys.campaigns.creators") }}
691
+ </div>
692
+ </div>
693
+ </div>
694
+
695
+ <div class="actions">
696
+ {{ $t("keys.campaigns.campaign.created_by") }}
697
+ <div class="company">
698
+ <div class="badge-tag">
699
+ <div class="image">
700
+ <img :src="campaign?.logo || '/imgs/no-cover-img.jpg'" alt="Logo" />
701
+ </div>
702
+ <div class="texts">
703
+ {{ campaign?.company || "" }}
704
+ <div class="small">{{ $t("keys.campaigns.campaign.rp_label") }}</div>
705
+ </div>
706
+ </div>
707
+ <div class="badge-tag" v-if="campaign?.email">
708
+ <div class="texts">
709
+ <div class="small">E-mail</div>
710
+ {{ campaign.email }}
711
+ </div>
712
+ </div>
713
+ <a v-if="campaign?.twitter" :href="campaign.twitter" target="_blank" rel="noopener noreferrer">
714
+ <div class="badge-tag w-48"><MGIcon icon="twitter-v2" /></div>
715
+ </a>
716
+ </div>
717
+ </div>
718
+ </div>
719
+ </div>
720
+ </div>
721
+ </div>
722
+ </div>
723
+ </template>
724
+
725
+ <style lang="scss" scoped>
726
+ #shared-campaign-detail {
727
+ align-items: center;
728
+ width: 100%;
729
+ justify-content: center;
730
+ overflow: auto;
731
+ height: 100%;
732
+
733
+ .container { max-width: 1140px !important; }
734
+
735
+ .header {
736
+ display: flex;
737
+ flex-direction: column;
738
+ align-items: flex-start;
739
+ gap: 43px;
740
+ margin-top: 12px;
741
+ margin-bottom: 43px;
742
+
743
+ .back {
744
+ display: flex;
745
+ align-items: center;
746
+ gap: 4px;
747
+ color: var(--card-article-title);
748
+ font-size: 12px;
749
+ cursor: pointer;
750
+ }
751
+ }
752
+
753
+ /* ---- Inline rescue/CTA banner (ported from TV UserRequestsValidation) ---- */
754
+ .rescue {
755
+ display: flex;
756
+ width: 100%;
757
+ padding: 12px;
758
+ align-items: center;
759
+ gap: 12px;
760
+ background-color: var(--button-secondary-default-bg);
761
+ justify-content: space-between;
762
+
763
+ .btn {
764
+ background: #D297FF;
765
+ display: flex;
766
+ height: 44px;
767
+ padding: 0 14px;
768
+ justify-content: center;
769
+ align-items: center;
770
+ gap: 4px;
771
+ color: #000;
772
+ cursor: pointer;
773
+ border: none;
774
+ &.disabled { opacity: 0.5; pointer-events: none; }
775
+ }
776
+
777
+ .rescue-actions {
778
+ display: flex;
779
+ flex-direction: column;
780
+ align-items: flex-end;
781
+ gap: 8px;
782
+ flex-shrink: 0;
783
+ }
784
+
785
+ .view-code-link {
786
+ display: inline-flex;
787
+ align-items: center;
788
+ gap: 6px;
789
+ font-size: 13px;
790
+ color: #D297FF;
791
+ cursor: pointer;
792
+ text-decoration: none;
793
+ white-space: nowrap;
794
+ &:hover { text-decoration: underline; }
795
+ }
796
+
797
+ .rescue-texts {
798
+ display: flex;
799
+ flex-direction: column;
800
+ gap: 12px;
801
+
802
+ .title {
803
+ display: flex;
804
+ flex-wrap: wrap;
805
+ gap: 4px;
806
+ font-size: 14px;
807
+ font-weight: 600;
808
+ color: var(--card-article-title);
809
+ span { color: #d297ff; }
810
+ .yellow { color: #FFD600; }
811
+ .red { color: #ff4800; }
812
+ }
813
+
814
+ .bar {
815
+ background-color: var(--bg-app-badge);
816
+ width: 346px;
817
+ height: 24px;
818
+ position: relative;
819
+
820
+ .progress {
821
+ height: 24px;
822
+ background-color: #d297ff;
823
+ span {
824
+ position: absolute;
825
+ right: 5px;
826
+ top: 5px;
827
+ font-size: 12px;
828
+ color: var(--secondary-info-fg);
829
+ }
830
+ }
831
+ .complete span { color: #272930; }
832
+ .yellow { background-color: #FFD600; }
833
+ }
834
+
835
+ .info {
836
+ font-size: 12px;
837
+ color: var(--secondary-info-fg);
838
+ i { color: #d297ff; }
839
+ .yellow { color: #FFD600; }
840
+ .red { color: #ff4800; }
841
+ }
842
+ }
843
+
844
+ @media (max-width: 768px) {
845
+ flex-direction: column;
846
+ .btn { width: 100%; }
847
+ .rescue-texts .bar { width: 100%; }
848
+ }
849
+ }
850
+
851
+ .featured-cover {
852
+ width: 100%;
853
+ max-height: 400px;
854
+ overflow: hidden;
855
+ margin-bottom: 24px;
856
+ img { width: 100%; height: 400px; object-fit: cover; object-position: center top; }
857
+ }
858
+
859
+ .content-key {
860
+ display: flex;
861
+ flex-direction: column;
862
+ gap: 24px;
863
+ padding-bottom: 43px;
864
+ border-bottom: 1px solid var(--button-secondary-default-bg);
865
+
866
+ .content-key-header {
867
+ display: flex;
868
+ flex-direction: column;
869
+ gap: 18px;
870
+
871
+ h4 { color: var(--card-article-title); font-size: 24px; font-weight: 700; }
872
+
873
+ .restriction-badges {
874
+ display: flex;
875
+ flex-wrap: wrap;
876
+ gap: 8px;
877
+
878
+ .restriction-badge {
879
+ display: inline-flex;
880
+ align-items: center;
881
+ gap: 6px;
882
+ padding: 4px 10px;
883
+ font-size: 12px;
884
+ font-weight: 600;
885
+ color: #D297FF;
886
+ background: rgba(210, 151, 255, 0.12);
887
+ border: 1px solid rgba(210, 151, 255, 0.35);
888
+ i { font-size: 13px; }
889
+ }
890
+ }
891
+ }
892
+
893
+ .cards {
894
+ display: flex;
895
+ align-items: flex-start;
896
+ gap: 24px;
897
+ width: 100%;
898
+
899
+ .side {
900
+ display: flex;
901
+ flex-direction: column;
902
+ gap: 32px;
903
+ max-width: 50%;
904
+ width: 50%;
905
+
906
+ .about {
907
+ display: flex;
908
+ flex-direction: column;
909
+ gap: 24px;
910
+
911
+ .game-cover {
912
+ display: flex;
913
+ align-items: center;
914
+ gap: 12px;
915
+
916
+ img { width: 144px; height: 188px; background-color: var(--button-secondary-default-bg); }
917
+
918
+ .info {
919
+ display: flex;
920
+ padding: 8px 12px;
921
+ flex-direction: column;
922
+ gap: 2px;
923
+ background-color: var(--button-secondary-default-bg);
924
+ font-size: 12px;
925
+ color: var(--secondary-info-fg);
926
+ span { color: var(--card-article-title); }
927
+ }
928
+ }
929
+ }
930
+ }
931
+
932
+ .side-border {
933
+ display: flex;
934
+ padding: 18px;
935
+ flex-direction: column;
936
+ gap: 24px;
937
+ border: 1px solid var(--button-secondary-default-bg);
938
+ width: 50%;
939
+
940
+ .status {
941
+ display: flex;
942
+ justify-content: space-between;
943
+ align-items: center;
944
+ width: 100%;
945
+ font-size: 14px;
946
+ color: var(--card-article-title);
947
+ font-weight: 600;
948
+
949
+ .tags {
950
+ display: flex;
951
+ align-items: center;
952
+ gap: 6px;
953
+
954
+ .tag, .tag-quantity {
955
+ background-color: var(--button-secondary-default-bg);
956
+ display: flex;
957
+ padding: 12px;
958
+ align-items: center;
959
+ gap: 4px;
960
+ height: 32px;
961
+ font-size: 12px;
962
+ font-weight: 400;
963
+ span.active { color: #42FF00; }
964
+ span.inactive { color: red; }
965
+ span.ending { color: #ffa600; }
966
+ }
967
+ }
968
+ }
969
+
970
+ .time {
971
+ display: flex;
972
+ gap: 12px;
973
+ width: 100%;
974
+
975
+ .card-time {
976
+ display: flex;
977
+ height: 80px;
978
+ padding: 24px;
979
+ flex-direction: column;
980
+ align-items: center;
981
+ gap: 6px;
982
+ background-color: var(--button-secondary-default-bg);
983
+ font-size: 28px;
984
+ color: #D297FF;
985
+ font-weight: 700;
986
+ width: -webkit-fill-available;
987
+ span { font-size: 10px; color: var(--secondary-info-fg); font-weight: 400; }
988
+ }
989
+ }
990
+
991
+ .requirements {
992
+ display: flex;
993
+ flex-direction: column;
994
+ gap: 12px;
995
+ font-size: 14px;
996
+ color: var(--card-article-title);
997
+ font-weight: 600;
998
+ width: 100%;
999
+
1000
+ .tags {
1001
+ display: flex;
1002
+ gap: 6px;
1003
+ width: 100%;
1004
+
1005
+ .tag {
1006
+ background-color: var(--button-secondary-default-bg);
1007
+ display: flex;
1008
+ padding: 12px;
1009
+ align-items: center;
1010
+ gap: 4px;
1011
+ justify-content: center;
1012
+ height: 32px;
1013
+ font-size: 12px;
1014
+ color: var(--secondary-info-fg);
1015
+ width: -webkit-fill-available;
1016
+ cursor: pointer;
1017
+ &:hover, &.active {
1018
+ background-color: #D297FF;
1019
+ color: #1C1C1C;
1020
+ i { color: #1C1C1C; }
1021
+ }
1022
+ }
1023
+ .tag-bg {
1024
+ background-color: var(--button-secondary-default-bg);
1025
+ display: flex;
1026
+ padding: 12px;
1027
+ align-items: center;
1028
+ height: 32px;
1029
+ font-size: 12px;
1030
+ color: var(--card-article-title);
1031
+ width: -webkit-fill-available;
1032
+ }
1033
+ }
1034
+ }
1035
+
1036
+ .actions {
1037
+ display: flex;
1038
+ flex-direction: column;
1039
+ gap: 12px;
1040
+ font-size: 14px;
1041
+ color: var(--card-article-title);
1042
+ font-weight: 600;
1043
+ width: 100%;
1044
+
1045
+ .tag {
1046
+ background-color: var(--button-secondary-default-bg);
1047
+ display: flex;
1048
+ padding: 12px;
1049
+ align-items: center;
1050
+ gap: 12px;
1051
+ font-size: 12px;
1052
+ color: var(--secondary-info-fg);
1053
+ width: -webkit-fill-available;
1054
+
1055
+ .icon {
1056
+ background-color: var(--bg-app-badge);
1057
+ width: 52px;
1058
+ height: 52px;
1059
+ align-content: center;
1060
+ flex-shrink: 0;
1061
+ border-radius: 100%;
1062
+ text-align-last: center;
1063
+ i { color: #D297FF; }
1064
+ }
1065
+ .texts {
1066
+ display: flex;
1067
+ flex-direction: column;
1068
+ gap: 4px;
1069
+ color: var(--card-article-title);
1070
+ font-size: 12px;
1071
+ .small { color: var(--secondary-info-fg); }
1072
+ }
1073
+ }
1074
+
1075
+ .empty-actions {
1076
+ font-size: 12px;
1077
+ font-weight: 400;
1078
+ color: var(--secondary-info-fg);
1079
+ padding: 12px;
1080
+ background-color: var(--button-secondary-default-bg);
1081
+ width: 100%;
1082
+ }
1083
+
1084
+ .reward span { color: #D297FF; }
1085
+
1086
+ .data {
1087
+ display: flex;
1088
+ gap: 12px;
1089
+ width: 100%;
1090
+ .info {
1091
+ display: flex;
1092
+ padding: 8px 12px;
1093
+ flex-direction: column;
1094
+ gap: 2px;
1095
+ background-color: var(--button-secondary-default-bg);
1096
+ font-size: 12px;
1097
+ color: var(--secondary-info-fg);
1098
+ width: -webkit-fill-available;
1099
+ span { color: var(--card-article-title); }
1100
+ }
1101
+ }
1102
+
1103
+ .users {
1104
+ display: flex;
1105
+ align-items: center;
1106
+ gap: 12px;
1107
+
1108
+ .imgs {
1109
+ display: flex;
1110
+ img { width: 50px; height: 50px; border: 2px solid var(--button-secondary-default-bg); }
1111
+ }
1112
+
1113
+ .text {
1114
+ font-size: 12px;
1115
+ color: var(--secondary-info-fg);
1116
+ width: -webkit-fill-available;
1117
+ span { font-size: 16px; color: #D297FF; font-weight: 600; }
1118
+ }
1119
+ }
1120
+
1121
+ .company {
1122
+ display: flex;
1123
+ gap: 12px;
1124
+ .w-48 { max-width: 48px; }
1125
+ .badge-tag {
1126
+ background-color: var(--button-secondary-default-bg);
1127
+ display: flex;
1128
+ padding: 8px 16px 8px 8px;
1129
+ align-items: center;
1130
+ gap: 12px;
1131
+ font-size: 12px;
1132
+ color: var(--secondary-info-fg);
1133
+ width: -webkit-fill-available;
1134
+ .image { width: 32px; height: 32px; img { width: 100%; border-radius: 100%; } }
1135
+ .texts {
1136
+ display: flex;
1137
+ flex-direction: column;
1138
+ gap: 2px;
1139
+ color: var(--card-article-title);
1140
+ font-size: 12px;
1141
+ .small { color: var(--secondary-info-fg); }
1142
+ }
1143
+ }
1144
+ }
1145
+ }
1146
+ }
1147
+ }
1148
+ }
1149
+
1150
+ @media (max-width: 768px) {
1151
+ .content-key .cards {
1152
+ flex-direction: column;
1153
+ .side, .side-border { max-width: 100%; width: 100%; }
1154
+ .side-border {
1155
+ .time { gap: 6px; .card-time { height: auto; padding: 8px; font-size: 24px; } }
1156
+ .actions .data { flex-direction: column; }
1157
+ .status { flex-direction: column; align-items: start; gap: 8px; }
1158
+ }
1159
+ }
1160
+ }
1161
+ }
1162
+ </style>