@mundogamernetwork/shared-ui 1.1.67 → 1.1.68
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/components/keys/KeyCampaignsCarousel.vue +190 -0
- package/locales/de.json +22 -2
- package/locales/en.json +22 -2
- package/locales/pt-BR.json +22 -2
- package/locales/ro.json +22 -2
- package/package.json +1 -1
- package/pages/key-campaign/[slug].vue +15 -0
- package/pages/key-campaigns/[slug].vue +18 -12
- package/pages/key-campaigns/redeem-key-approved.vue +311 -0
- package/pages/key-campaigns/redeem-key.vue +320 -0
- package/services/campaignService.ts +12 -4
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, onMounted, inject } from "vue"
|
|
3
|
+
import { fetchCampaigns } from "../../services/campaignService"
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
excludeId?: number | null
|
|
7
|
+
mgPlatform?: string
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
const mgPlatform = props.mgPlatform || (inject<string>("mgPlatform", "MGTV"))
|
|
11
|
+
|
|
12
|
+
const items = ref<any[]>([])
|
|
13
|
+
const loading = ref(true)
|
|
14
|
+
|
|
15
|
+
onMounted(async () => {
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetchCampaigns(
|
|
18
|
+
{
|
|
19
|
+
filter: { is_open: 1 },
|
|
20
|
+
per_page: 10,
|
|
21
|
+
sort: "id",
|
|
22
|
+
order: "desc",
|
|
23
|
+
},
|
|
24
|
+
mgPlatform,
|
|
25
|
+
)
|
|
26
|
+
const data: any[] = res?.data?.data || res?.data || []
|
|
27
|
+
items.value = props.excludeId
|
|
28
|
+
? data.filter((c: any) => c.id !== props.excludeId)
|
|
29
|
+
: data
|
|
30
|
+
} catch {
|
|
31
|
+
// silently ignore — carousel is non-critical
|
|
32
|
+
} finally {
|
|
33
|
+
loading.value = false
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<template>
|
|
39
|
+
<div v-if="loading || items.length > 0" class="kc-carousel">
|
|
40
|
+
<div class="kc-carousel__header">
|
|
41
|
+
<span class="kc-carousel__title">{{ $t("keys.campaigns.other_campaigns") }}</span>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div v-if="loading" class="kc-carousel__track">
|
|
45
|
+
<div v-for="i in 4" :key="i" class="kc-carousel__skeleton" />
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div v-else class="kc-carousel__track">
|
|
49
|
+
<NuxtLink
|
|
50
|
+
v-for="item in items"
|
|
51
|
+
:key="item.id"
|
|
52
|
+
:to="`/key-campaigns/${item.slug}`"
|
|
53
|
+
class="kc-carousel__card"
|
|
54
|
+
>
|
|
55
|
+
<div class="kc-carousel__thumb">
|
|
56
|
+
<img
|
|
57
|
+
v-if="item.cover_src || item.cover"
|
|
58
|
+
:src="item.cover_src || item.cover"
|
|
59
|
+
:alt="item.name"
|
|
60
|
+
/>
|
|
61
|
+
<div v-else class="kc-carousel__thumb-placeholder">
|
|
62
|
+
<i class="mg-icon mg-icon-game-key" />
|
|
63
|
+
</div>
|
|
64
|
+
<span v-if="!item.is_open" class="kc-carousel__closed">{{ $t("keys.campaigns.closed") }}</span>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="kc-carousel__info">
|
|
67
|
+
<div class="kc-carousel__name">{{ item.name }}</div>
|
|
68
|
+
<div class="kc-carousel__avail">
|
|
69
|
+
{{ item.available_count ?? 0 }}/{{ item.total_count ?? item.quantity ?? "?" }}
|
|
70
|
+
{{ $t("keys.campaigns.available") }}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</NuxtLink>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<style scoped lang="scss">
|
|
79
|
+
.kc-carousel {
|
|
80
|
+
margin-top: 40px;
|
|
81
|
+
padding-top: 24px;
|
|
82
|
+
border-top: 1px solid var(--button-secondary-default-bg);
|
|
83
|
+
|
|
84
|
+
&__header {
|
|
85
|
+
margin-bottom: 16px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
&__title {
|
|
89
|
+
font-size: 16px;
|
|
90
|
+
font-weight: 700;
|
|
91
|
+
color: var(--card-article-title);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
&__track {
|
|
95
|
+
display: flex;
|
|
96
|
+
gap: 12px;
|
|
97
|
+
overflow-x: auto;
|
|
98
|
+
padding-bottom: 8px;
|
|
99
|
+
scrollbar-width: thin;
|
|
100
|
+
scrollbar-color: var(--button-secondary-default-bg) transparent;
|
|
101
|
+
|
|
102
|
+
&::-webkit-scrollbar { height: 4px; }
|
|
103
|
+
&::-webkit-scrollbar-track { background: transparent; }
|
|
104
|
+
&::-webkit-scrollbar-thumb { background: var(--button-secondary-default-bg); }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
&__skeleton {
|
|
108
|
+
flex: 0 0 180px;
|
|
109
|
+
height: 140px;
|
|
110
|
+
background: var(--bg-app-badge);
|
|
111
|
+
animation: pulse 1.4s ease-in-out infinite;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
&__card {
|
|
115
|
+
flex: 0 0 180px;
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
text-decoration: none;
|
|
119
|
+
color: inherit;
|
|
120
|
+
border: 1px solid var(--button-secondary-default-bg);
|
|
121
|
+
transition: border-color 0.15s;
|
|
122
|
+
|
|
123
|
+
&:hover { border-color: var(--key-accent, var(--primary, #D297FF)); }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
&__thumb {
|
|
127
|
+
position: relative;
|
|
128
|
+
width: 100%;
|
|
129
|
+
height: 101px;
|
|
130
|
+
overflow: hidden;
|
|
131
|
+
background: var(--bg-app-badge);
|
|
132
|
+
|
|
133
|
+
img {
|
|
134
|
+
width: 100%;
|
|
135
|
+
height: 100%;
|
|
136
|
+
object-fit: cover;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
&__thumb-placeholder {
|
|
141
|
+
width: 100%;
|
|
142
|
+
height: 100%;
|
|
143
|
+
display: flex;
|
|
144
|
+
align-items: center;
|
|
145
|
+
justify-content: center;
|
|
146
|
+
color: var(--secondary-info-fg);
|
|
147
|
+
font-size: 24px;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
&__closed {
|
|
151
|
+
position: absolute;
|
|
152
|
+
inset: 0;
|
|
153
|
+
display: flex;
|
|
154
|
+
align-items: center;
|
|
155
|
+
justify-content: center;
|
|
156
|
+
background: rgba(0, 0, 0, 0.55);
|
|
157
|
+
font-size: 11px;
|
|
158
|
+
font-weight: 700;
|
|
159
|
+
color: #fff;
|
|
160
|
+
letter-spacing: 0.5px;
|
|
161
|
+
text-transform: uppercase;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
&__info {
|
|
165
|
+
padding: 8px 10px;
|
|
166
|
+
display: flex;
|
|
167
|
+
flex-direction: column;
|
|
168
|
+
gap: 4px;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
&__name {
|
|
172
|
+
font-size: 12px;
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
color: var(--card-article-title);
|
|
175
|
+
overflow: hidden;
|
|
176
|
+
text-overflow: ellipsis;
|
|
177
|
+
white-space: nowrap;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
&__avail {
|
|
181
|
+
font-size: 11px;
|
|
182
|
+
color: var(--secondary-info-fg);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
@keyframes pulse {
|
|
187
|
+
0%, 100% { opacity: 1; }
|
|
188
|
+
50% { opacity: 0.4; }
|
|
189
|
+
}
|
|
190
|
+
</style>
|
package/locales/de.json
CHANGED
|
@@ -133,7 +133,27 @@
|
|
|
133
133
|
"who_redeem": "Who redeemed",
|
|
134
134
|
"created_by": "Published by",
|
|
135
135
|
"no_data": "Campaign not found"
|
|
136
|
-
}
|
|
136
|
+
},
|
|
137
|
+
"other_campaigns": "Andere Kampagnen",
|
|
138
|
+
"available": "verfügbar",
|
|
139
|
+
"closed": "Beendet",
|
|
140
|
+
"platform": "Plattform",
|
|
141
|
+
"region": "Region",
|
|
142
|
+
"description_optional": "Beschreibung (optional)",
|
|
143
|
+
"description_placeholder": "Erzähl uns etwas über deinen Kanal...",
|
|
144
|
+
"sending": "Wird gesendet...",
|
|
145
|
+
"request_sent": "Anfrage gesendet! Weiterleitung...",
|
|
146
|
+
"error_loading": "Kampagnendaten konnten nicht geladen werden.",
|
|
147
|
+
"error_request": "Anfrage konnte nicht gesendet werden. Bitte erneut versuchen.",
|
|
148
|
+
"error_reveal": "Schlüssel konnte nicht enthüllt werden.",
|
|
149
|
+
"redeem_title": "Schlüssel anfordern",
|
|
150
|
+
"reveal_hint": "Klicke unten, um deinen Schlüsselcode zu enthüllen.",
|
|
151
|
+
"your_key": "Dein Schlüssel",
|
|
152
|
+
"copy": "Kopieren",
|
|
153
|
+
"copied": "Kopiert!",
|
|
154
|
+
"redeem_on_platform": "Löse diesen Schlüssel auf der entsprechenden Plattform ein.",
|
|
155
|
+
"revealing": "Enthülle...",
|
|
156
|
+
"status": "Status"
|
|
137
157
|
},
|
|
138
158
|
"materials": {
|
|
139
159
|
"title": "Submit material",
|
|
@@ -158,4 +178,4 @@
|
|
|
158
178
|
"success_close": "Close"
|
|
159
179
|
}
|
|
160
180
|
}
|
|
161
|
-
}
|
|
181
|
+
}
|
package/locales/en.json
CHANGED
|
@@ -133,7 +133,27 @@
|
|
|
133
133
|
"who_redeem": "Who redeemed",
|
|
134
134
|
"created_by": "Published by",
|
|
135
135
|
"no_data": "Campaign not found"
|
|
136
|
-
}
|
|
136
|
+
},
|
|
137
|
+
"other_campaigns": "Other campaigns",
|
|
138
|
+
"available": "available",
|
|
139
|
+
"closed": "Closed",
|
|
140
|
+
"platform": "Platform",
|
|
141
|
+
"region": "Region",
|
|
142
|
+
"description_optional": "Description (optional)",
|
|
143
|
+
"description_placeholder": "Tell us a bit about your channel or content plans...",
|
|
144
|
+
"sending": "Sending...",
|
|
145
|
+
"request_sent": "Request sent! Redirecting...",
|
|
146
|
+
"error_loading": "Failed to load campaign data.",
|
|
147
|
+
"error_request": "Could not submit request. Please try again.",
|
|
148
|
+
"error_reveal": "Could not reveal key. Please try again.",
|
|
149
|
+
"redeem_title": "Request a Key",
|
|
150
|
+
"reveal_hint": "Click the button below to reveal your key code.",
|
|
151
|
+
"your_key": "Your Key",
|
|
152
|
+
"copy": "Copy",
|
|
153
|
+
"copied": "Copied!",
|
|
154
|
+
"redeem_on_platform": "Redeem this key on the platform where you received it.",
|
|
155
|
+
"revealing": "Revealing...",
|
|
156
|
+
"status": "Status"
|
|
137
157
|
},
|
|
138
158
|
"materials": {
|
|
139
159
|
"title": "Submit material",
|
|
@@ -158,4 +178,4 @@
|
|
|
158
178
|
"success_close": "Close"
|
|
159
179
|
}
|
|
160
180
|
}
|
|
161
|
-
}
|
|
181
|
+
}
|
package/locales/pt-BR.json
CHANGED
|
@@ -133,7 +133,27 @@
|
|
|
133
133
|
"who_redeem": "Who redeemed",
|
|
134
134
|
"created_by": "Published by",
|
|
135
135
|
"no_data": "Campaign not found"
|
|
136
|
-
}
|
|
136
|
+
},
|
|
137
|
+
"other_campaigns": "Outras campanhas",
|
|
138
|
+
"available": "disponível",
|
|
139
|
+
"closed": "Encerrada",
|
|
140
|
+
"platform": "Plataforma",
|
|
141
|
+
"region": "Região",
|
|
142
|
+
"description_optional": "Descrição (opcional)",
|
|
143
|
+
"description_placeholder": "Conte um pouco sobre seu canal ou planos de conteúdo...",
|
|
144
|
+
"sending": "Enviando...",
|
|
145
|
+
"request_sent": "Solicitação enviada! Redirecionando...",
|
|
146
|
+
"error_loading": "Falha ao carregar dados da campanha.",
|
|
147
|
+
"error_request": "Não foi possível enviar a solicitação. Tente novamente.",
|
|
148
|
+
"error_reveal": "Não foi possível revelar a chave. Tente novamente.",
|
|
149
|
+
"redeem_title": "Solicitar uma Chave",
|
|
150
|
+
"reveal_hint": "Clique no botão abaixo para revelar o código da sua chave.",
|
|
151
|
+
"your_key": "Sua Chave",
|
|
152
|
+
"copy": "Copiar",
|
|
153
|
+
"copied": "Copiado!",
|
|
154
|
+
"redeem_on_platform": "Resgate esta chave na plataforma correspondente.",
|
|
155
|
+
"revealing": "Revelando...",
|
|
156
|
+
"status": "Status"
|
|
137
157
|
},
|
|
138
158
|
"materials": {
|
|
139
159
|
"title": "Submit material",
|
|
@@ -158,4 +178,4 @@
|
|
|
158
178
|
"success_close": "Close"
|
|
159
179
|
}
|
|
160
180
|
}
|
|
161
|
-
}
|
|
181
|
+
}
|
package/locales/ro.json
CHANGED
|
@@ -133,7 +133,27 @@
|
|
|
133
133
|
"who_redeem": "Who redeemed",
|
|
134
134
|
"created_by": "Published by",
|
|
135
135
|
"no_data": "Campaign not found"
|
|
136
|
-
}
|
|
136
|
+
},
|
|
137
|
+
"other_campaigns": "Alte campanii",
|
|
138
|
+
"available": "disponibil",
|
|
139
|
+
"closed": "Închis",
|
|
140
|
+
"platform": "Platformă",
|
|
141
|
+
"region": "Regiune",
|
|
142
|
+
"description_optional": "Descriere (opțional)",
|
|
143
|
+
"description_placeholder": "Spune-ne puțin despre canalul tău...",
|
|
144
|
+
"sending": "Se trimite...",
|
|
145
|
+
"request_sent": "Cerere trimisă! Redirecționare...",
|
|
146
|
+
"error_loading": "Eroare la încărcarea datelor campaniei.",
|
|
147
|
+
"error_request": "Nu s-a putut trimite cererea. Încearcă din nou.",
|
|
148
|
+
"error_reveal": "Nu s-a putut dezvălui cheia.",
|
|
149
|
+
"redeem_title": "Solicită o Cheie",
|
|
150
|
+
"reveal_hint": "Apasă butonul de mai jos pentru a dezvălui codul cheii.",
|
|
151
|
+
"your_key": "Cheia Ta",
|
|
152
|
+
"copy": "Copiază",
|
|
153
|
+
"copied": "Copiat!",
|
|
154
|
+
"redeem_on_platform": "Activează această cheie pe platforma corespunzătoare.",
|
|
155
|
+
"revealing": "Se dezvăluie...",
|
|
156
|
+
"status": "Status"
|
|
137
157
|
},
|
|
138
158
|
"materials": {
|
|
139
159
|
"title": "Submit material",
|
|
@@ -158,4 +178,4 @@
|
|
|
158
178
|
"success_close": "Close"
|
|
159
179
|
}
|
|
160
180
|
}
|
|
161
|
-
}
|
|
181
|
+
}
|
package/package.json
CHANGED
|
@@ -41,6 +41,19 @@ const key = computed(() => keyData.value?.data || null);
|
|
|
41
41
|
const loading = pending;
|
|
42
42
|
const error = computed(() => !!fetchError.value || (!pending.value && !key.value));
|
|
43
43
|
|
|
44
|
+
const { $i18n } = useNuxtApp()
|
|
45
|
+
const t = $i18n.t.bind($i18n)
|
|
46
|
+
|
|
47
|
+
useSeoMeta({
|
|
48
|
+
title: () => key.value?.name ? t('keys.campaigns.seo.title', { game: key.value.name }) : t('keys.campaigns.seo.title_generic'),
|
|
49
|
+
description: () => key.value?.name ? t('keys.campaigns.seo.description', { game: key.value.name }) : t('keys.campaigns.seo.title_generic'),
|
|
50
|
+
ogTitle: () => key.value?.name ? t('keys.campaigns.seo.title', { game: key.value.name }) : t('keys.campaigns.seo.title_generic'),
|
|
51
|
+
ogDescription: () => key.value?.name ? t('keys.campaigns.seo.description', { game: key.value.name }) : t('keys.campaigns.seo.title_generic'),
|
|
52
|
+
ogImage: () => key.value?.cover_src || key.value?.cover || undefined,
|
|
53
|
+
ogType: 'website',
|
|
54
|
+
twitterCard: 'summary_large_image',
|
|
55
|
+
})
|
|
56
|
+
|
|
44
57
|
const restriction = computed(() => {
|
|
45
58
|
const k = key.value;
|
|
46
59
|
if (!k) return '';
|
|
@@ -76,6 +89,8 @@ const restriction = computed(() => {
|
|
|
76
89
|
:game-id="key.game?.id || key.game_id"
|
|
77
90
|
/>
|
|
78
91
|
</div>
|
|
92
|
+
|
|
93
|
+
<KeyCampaignsCarousel :exclude-id="key.id ?? null" />
|
|
79
94
|
</template>
|
|
80
95
|
</div>
|
|
81
96
|
</template>
|
|
@@ -681,8 +681,9 @@ onMounted(async () => {
|
|
|
681
681
|
<img
|
|
682
682
|
v-for="(u, index) in redeemedUsers?.users?.slice(0, 4)"
|
|
683
683
|
:key="index"
|
|
684
|
-
:src="u.avatar_url"
|
|
684
|
+
:src="u.avatar_url || '/imgs/avatar-default.jpg'"
|
|
685
685
|
:alt="u.nickname"
|
|
686
|
+
@error="(e: Event) => { const t = e.target as HTMLImageElement; if (t && t.src !== '/imgs/avatar-default.jpg') t.src = '/imgs/avatar-default.jpg' }"
|
|
686
687
|
/>
|
|
687
688
|
</div>
|
|
688
689
|
<div class="text">
|
|
@@ -719,6 +720,11 @@ onMounted(async () => {
|
|
|
719
720
|
</div>
|
|
720
721
|
</div>
|
|
721
722
|
</div>
|
|
723
|
+
<KeyCampaignsCarousel
|
|
724
|
+
:exclude-id="campaign?.id ?? null"
|
|
725
|
+
:mg-platform="mgPlatform"
|
|
726
|
+
class="container"
|
|
727
|
+
/>
|
|
722
728
|
</div>
|
|
723
729
|
</template>
|
|
724
730
|
|
|
@@ -761,7 +767,7 @@ onMounted(async () => {
|
|
|
761
767
|
justify-content: space-between;
|
|
762
768
|
|
|
763
769
|
.btn {
|
|
764
|
-
background: var(--key-accent, #D297FF);
|
|
770
|
+
background: var(--key-accent, var(--primary, #D297FF));
|
|
765
771
|
display: flex;
|
|
766
772
|
height: 44px;
|
|
767
773
|
padding: 0 14px;
|
|
@@ -787,7 +793,7 @@ onMounted(async () => {
|
|
|
787
793
|
align-items: center;
|
|
788
794
|
gap: 6px;
|
|
789
795
|
font-size: 13px;
|
|
790
|
-
color: var(--key-accent, #D297FF);
|
|
796
|
+
color: var(--key-accent, var(--primary, #D297FF));
|
|
791
797
|
cursor: pointer;
|
|
792
798
|
text-decoration: none;
|
|
793
799
|
white-space: nowrap;
|
|
@@ -806,7 +812,7 @@ onMounted(async () => {
|
|
|
806
812
|
font-size: 14px;
|
|
807
813
|
font-weight: 600;
|
|
808
814
|
color: var(--card-article-title);
|
|
809
|
-
span { color: var(--key-accent, #
|
|
815
|
+
span { color: var(--key-accent, var(--primary, #D297FF)); }
|
|
810
816
|
.yellow { color: #FFD600; }
|
|
811
817
|
.red { color: #ff4800; }
|
|
812
818
|
}
|
|
@@ -819,7 +825,7 @@ onMounted(async () => {
|
|
|
819
825
|
|
|
820
826
|
.progress {
|
|
821
827
|
height: 24px;
|
|
822
|
-
background-color: var(--key-accent, #
|
|
828
|
+
background-color: var(--key-accent, var(--primary, #D297FF));
|
|
823
829
|
span {
|
|
824
830
|
position: absolute;
|
|
825
831
|
right: 5px;
|
|
@@ -835,7 +841,7 @@ onMounted(async () => {
|
|
|
835
841
|
.info {
|
|
836
842
|
font-size: 12px;
|
|
837
843
|
color: var(--secondary-info-fg);
|
|
838
|
-
i { color: var(--key-accent, #
|
|
844
|
+
i { color: var(--key-accent, var(--primary, #D297FF)); }
|
|
839
845
|
.yellow { color: #FFD600; }
|
|
840
846
|
.red { color: #ff4800; }
|
|
841
847
|
}
|
|
@@ -882,7 +888,7 @@ onMounted(async () => {
|
|
|
882
888
|
padding: 4px 10px;
|
|
883
889
|
font-size: 12px;
|
|
884
890
|
font-weight: 600;
|
|
885
|
-
color: var(--key-accent, #D297FF);
|
|
891
|
+
color: var(--key-accent, var(--primary, #D297FF));
|
|
886
892
|
background: rgba(210, 151, 255, 0.12);
|
|
887
893
|
border: 1px solid rgba(210, 151, 255, 0.35);
|
|
888
894
|
i { font-size: 13px; }
|
|
@@ -981,7 +987,7 @@ onMounted(async () => {
|
|
|
981
987
|
gap: 6px;
|
|
982
988
|
background-color: var(--button-secondary-default-bg);
|
|
983
989
|
font-size: 28px;
|
|
984
|
-
color: var(--key-accent, #D297FF);
|
|
990
|
+
color: var(--key-accent, var(--primary, #D297FF));
|
|
985
991
|
font-weight: 700;
|
|
986
992
|
width: -webkit-fill-available;
|
|
987
993
|
span { font-size: 10px; color: var(--secondary-info-fg); font-weight: 400; }
|
|
@@ -1015,7 +1021,7 @@ onMounted(async () => {
|
|
|
1015
1021
|
width: -webkit-fill-available;
|
|
1016
1022
|
cursor: pointer;
|
|
1017
1023
|
&:hover, &.active {
|
|
1018
|
-
background-color: var(--key-accent, #D297FF);
|
|
1024
|
+
background-color: var(--key-accent, var(--primary, #D297FF));
|
|
1019
1025
|
color: #1C1C1C;
|
|
1020
1026
|
i { color: #1C1C1C; }
|
|
1021
1027
|
}
|
|
@@ -1060,7 +1066,7 @@ onMounted(async () => {
|
|
|
1060
1066
|
flex-shrink: 0;
|
|
1061
1067
|
border-radius: 100%;
|
|
1062
1068
|
text-align-last: center;
|
|
1063
|
-
i { color: var(--key-accent, #D297FF); }
|
|
1069
|
+
i { color: var(--key-accent, var(--primary, #D297FF)); }
|
|
1064
1070
|
}
|
|
1065
1071
|
.texts {
|
|
1066
1072
|
display: flex;
|
|
@@ -1081,7 +1087,7 @@ onMounted(async () => {
|
|
|
1081
1087
|
width: 100%;
|
|
1082
1088
|
}
|
|
1083
1089
|
|
|
1084
|
-
.reward span { color: var(--key-accent, #D297FF); }
|
|
1090
|
+
.reward span { color: var(--key-accent, var(--primary, #D297FF)); }
|
|
1085
1091
|
|
|
1086
1092
|
.data {
|
|
1087
1093
|
display: flex;
|
|
@@ -1114,7 +1120,7 @@ onMounted(async () => {
|
|
|
1114
1120
|
font-size: 12px;
|
|
1115
1121
|
color: var(--secondary-info-fg);
|
|
1116
1122
|
width: -webkit-fill-available;
|
|
1117
|
-
span { font-size: 16px; color: var(--key-accent, #D297FF); font-weight: 600; }
|
|
1123
|
+
span { font-size: 16px; color: var(--key-accent, var(--primary, #D297FF)); font-weight: 600; }
|
|
1118
1124
|
}
|
|
1119
1125
|
}
|
|
1120
1126
|
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, inject, onMounted } from "vue"
|
|
3
|
+
import { useRoute, navigateTo } from "#app"
|
|
4
|
+
import { useNuxtApp } from "#app"
|
|
5
|
+
import { revealKey, fetchKeyRequestById } from "../../services/campaignService"
|
|
6
|
+
|
|
7
|
+
const route = useRoute()
|
|
8
|
+
const { $i18n } = useNuxtApp()
|
|
9
|
+
const locale = $i18n.locale
|
|
10
|
+
|
|
11
|
+
const currentUser = inject<any>("currentUser", null)
|
|
12
|
+
const loginUrl = inject<string>("loginUrl", "/")
|
|
13
|
+
|
|
14
|
+
const requestId = Number(route.query.requestId)
|
|
15
|
+
const keyItemsParam = route.query.keyItems as string | undefined
|
|
16
|
+
|
|
17
|
+
const loading = ref(true)
|
|
18
|
+
const revealing = ref(false)
|
|
19
|
+
const keyCode = ref<string | null>(null)
|
|
20
|
+
const error = ref("")
|
|
21
|
+
const requestData = ref<any>(null)
|
|
22
|
+
const copied = ref(false)
|
|
23
|
+
|
|
24
|
+
onMounted(async () => {
|
|
25
|
+
if (!currentUser) {
|
|
26
|
+
const returnTo = typeof window !== "undefined" ? window.location.href : ""
|
|
27
|
+
navigateTo(`${loginUrl}?redirect_to=${encodeURIComponent(returnTo)}`, { external: true })
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
if (!requestId) {
|
|
31
|
+
navigateTo(`/${locale.value}/key-campaigns`)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const res = await fetchKeyRequestById(requestId)
|
|
36
|
+
requestData.value = res?.data?.data || res?.data || null
|
|
37
|
+
} catch {
|
|
38
|
+
error.value = $i18n.t("keys.campaigns.error_loading")
|
|
39
|
+
} finally {
|
|
40
|
+
loading.value = false
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
async function reveal() {
|
|
45
|
+
revealing.value = true
|
|
46
|
+
error.value = ""
|
|
47
|
+
try {
|
|
48
|
+
const res = await revealKey(requestId)
|
|
49
|
+
const data = res?.data?.data || res?.data || null
|
|
50
|
+
keyCode.value = data?.key || data?.code || data?.value || null
|
|
51
|
+
if (!keyCode.value) {
|
|
52
|
+
error.value = $i18n.t("keys.campaigns.error_reveal")
|
|
53
|
+
}
|
|
54
|
+
} catch (e: any) {
|
|
55
|
+
error.value = e?.response?.data?.message || $i18n.t("keys.campaigns.error_reveal")
|
|
56
|
+
} finally {
|
|
57
|
+
revealing.value = false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function copyCode() {
|
|
62
|
+
if (!keyCode.value) return
|
|
63
|
+
try {
|
|
64
|
+
await navigator.clipboard.writeText(keyCode.value)
|
|
65
|
+
copied.value = true
|
|
66
|
+
setTimeout(() => { copied.value = false }, 2000)
|
|
67
|
+
} catch {
|
|
68
|
+
// fallback: select text
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function goBack() {
|
|
73
|
+
if (typeof window !== "undefined" && window.history.length > 1) {
|
|
74
|
+
window.history.back()
|
|
75
|
+
} else {
|
|
76
|
+
navigateTo(`/${locale.value}/key-campaigns`)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</script>
|
|
80
|
+
|
|
81
|
+
<template>
|
|
82
|
+
<div id="shared-redeem-key-approved">
|
|
83
|
+
<div class="container">
|
|
84
|
+
<div class="header">
|
|
85
|
+
<NuxtLink @click.prevent="goBack" class="back">
|
|
86
|
+
<MGIcon size="2x" icon="chevron-left" />
|
|
87
|
+
<span>{{ $t("keys.campaigns.back") }}</span>
|
|
88
|
+
</NuxtLink>
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<div v-if="loading" class="state">
|
|
92
|
+
<div class="skeleton" v-for="i in 2" :key="i" />
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div v-else-if="error && !keyCode" class="state error">{{ error }}</div>
|
|
96
|
+
|
|
97
|
+
<div v-else class="content">
|
|
98
|
+
<div class="title-bar">
|
|
99
|
+
<MGIcon icon="version" size="2x" class="title-icon" />
|
|
100
|
+
<div>
|
|
101
|
+
<h3>{{ $t("keys.campaigns.status_key.reveal_key") }}</h3>
|
|
102
|
+
<p v-if="requestData?.key?.name" class="campaign-name">{{ requestData.key.name }}</p>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<!-- Key not yet revealed -->
|
|
107
|
+
<div v-if="!keyCode" class="reveal-block">
|
|
108
|
+
<p class="hint">{{ $t("keys.campaigns.reveal_hint") }}</p>
|
|
109
|
+
<button
|
|
110
|
+
class="btn"
|
|
111
|
+
:class="{ disabled: revealing }"
|
|
112
|
+
:disabled="revealing"
|
|
113
|
+
@click="reveal"
|
|
114
|
+
>
|
|
115
|
+
{{ revealing ? $t("keys.campaigns.revealing") : $t("keys.campaigns.status_key.reveal_key") }}
|
|
116
|
+
</button>
|
|
117
|
+
<div v-if="error" class="req-error">{{ error }}</div>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
<!-- Key revealed -->
|
|
121
|
+
<div v-else class="key-block">
|
|
122
|
+
<div class="key-label">{{ $t("keys.campaigns.your_key") }}</div>
|
|
123
|
+
<div class="key-code-row">
|
|
124
|
+
<span class="key-code">{{ keyCode }}</span>
|
|
125
|
+
<button class="copy-btn" @click="copyCode">
|
|
126
|
+
<MGIcon :icon="copied ? 'check' : 'copy'" size="1x" />
|
|
127
|
+
{{ copied ? $t("keys.campaigns.copied") : $t("keys.campaigns.copy") }}
|
|
128
|
+
</button>
|
|
129
|
+
</div>
|
|
130
|
+
<p class="redeem-info">{{ $t("keys.campaigns.redeem_on_platform") }}</p>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<!-- Request info -->
|
|
134
|
+
<div v-if="requestData" class="info-grid">
|
|
135
|
+
<div class="info-row" v-if="requestData.status_request?.name">
|
|
136
|
+
<span class="info-label">{{ $t("keys.campaigns.status") }}</span>
|
|
137
|
+
<span class="info-val">{{ requestData.status_request.name }}</span>
|
|
138
|
+
</div>
|
|
139
|
+
<div class="info-row" v-if="requestData.key?.name">
|
|
140
|
+
<span class="info-label">{{ $t("keys.campaigns.campaign.title") }}</span>
|
|
141
|
+
<span class="info-val">{{ requestData.key.name }}</span>
|
|
142
|
+
</div>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</template>
|
|
148
|
+
|
|
149
|
+
<style lang="scss" scoped>
|
|
150
|
+
#shared-redeem-key-approved {
|
|
151
|
+
align-items: center;
|
|
152
|
+
width: 100%;
|
|
153
|
+
justify-content: center;
|
|
154
|
+
overflow: auto;
|
|
155
|
+
height: 100%;
|
|
156
|
+
|
|
157
|
+
.container { max-width: 680px !important; padding: 32px 16px 64px; }
|
|
158
|
+
|
|
159
|
+
.header {
|
|
160
|
+
margin-bottom: 24px;
|
|
161
|
+
.back {
|
|
162
|
+
display: inline-flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
gap: 8px;
|
|
165
|
+
font-size: 14px;
|
|
166
|
+
color: var(--secondary-info-fg);
|
|
167
|
+
cursor: pointer;
|
|
168
|
+
text-decoration: none;
|
|
169
|
+
&:hover { color: var(--card-article-title); }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.state {
|
|
174
|
+
display: flex;
|
|
175
|
+
flex-direction: column;
|
|
176
|
+
align-items: center;
|
|
177
|
+
gap: 12px;
|
|
178
|
+
padding: 64px 0;
|
|
179
|
+
text-align: center;
|
|
180
|
+
color: var(--secondary-info-fg);
|
|
181
|
+
&.error { color: #ff4800; }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.skeleton {
|
|
185
|
+
width: 100%;
|
|
186
|
+
height: 48px;
|
|
187
|
+
background: var(--bg-app-badge);
|
|
188
|
+
animation: pulse 1.4s ease-in-out infinite;
|
|
189
|
+
& + & { margin-top: 8px; }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.content { display: flex; flex-direction: column; gap: 28px; }
|
|
193
|
+
|
|
194
|
+
.title-bar {
|
|
195
|
+
display: flex;
|
|
196
|
+
align-items: flex-start;
|
|
197
|
+
gap: 14px;
|
|
198
|
+
.title-icon { color: var(--key-accent, var(--primary, #D297FF)); margin-top: 2px; }
|
|
199
|
+
h3 { font-size: 20px; font-weight: 700; color: var(--card-article-title); margin: 0 0 4px; }
|
|
200
|
+
.campaign-name { font-size: 13px; color: var(--secondary-info-fg); margin: 0; }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.reveal-block {
|
|
204
|
+
display: flex;
|
|
205
|
+
flex-direction: column;
|
|
206
|
+
gap: 16px;
|
|
207
|
+
padding: 24px;
|
|
208
|
+
background: var(--bg-app-badge);
|
|
209
|
+
border: 1px solid var(--button-secondary-default-bg);
|
|
210
|
+
|
|
211
|
+
.hint { font-size: 14px; color: var(--secondary-info-fg); margin: 0; }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.key-block {
|
|
215
|
+
display: flex;
|
|
216
|
+
flex-direction: column;
|
|
217
|
+
gap: 12px;
|
|
218
|
+
padding: 24px;
|
|
219
|
+
background: var(--bg-app-badge);
|
|
220
|
+
border: 1px solid var(--key-accent, var(--primary, #D297FF));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.key-label {
|
|
224
|
+
font-size: 12px;
|
|
225
|
+
font-weight: 600;
|
|
226
|
+
text-transform: uppercase;
|
|
227
|
+
letter-spacing: 0.5px;
|
|
228
|
+
color: var(--secondary-info-fg);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.key-code-row {
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: center;
|
|
234
|
+
gap: 12px;
|
|
235
|
+
flex-wrap: wrap;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.key-code {
|
|
239
|
+
font-size: 20px;
|
|
240
|
+
font-weight: 700;
|
|
241
|
+
font-family: monospace;
|
|
242
|
+
letter-spacing: 2px;
|
|
243
|
+
color: var(--card-article-title);
|
|
244
|
+
word-break: break-all;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.copy-btn {
|
|
248
|
+
display: inline-flex;
|
|
249
|
+
align-items: center;
|
|
250
|
+
gap: 6px;
|
|
251
|
+
padding: 6px 14px;
|
|
252
|
+
font-size: 13px;
|
|
253
|
+
font-weight: 600;
|
|
254
|
+
background: transparent;
|
|
255
|
+
border: 1px solid var(--key-accent, var(--primary, #D297FF));
|
|
256
|
+
color: var(--key-accent, var(--primary, #D297FF));
|
|
257
|
+
cursor: pointer;
|
|
258
|
+
transition: background 0.15s;
|
|
259
|
+
&:hover { background: rgba(210, 151, 255, 0.1); }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.redeem-info {
|
|
263
|
+
font-size: 12px;
|
|
264
|
+
color: var(--secondary-info-fg);
|
|
265
|
+
margin: 0;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
.req-error { font-size: 13px; color: #ff4800; }
|
|
269
|
+
|
|
270
|
+
.info-grid {
|
|
271
|
+
display: flex;
|
|
272
|
+
flex-direction: column;
|
|
273
|
+
gap: 8px;
|
|
274
|
+
padding-top: 8px;
|
|
275
|
+
border-top: 1px solid var(--button-secondary-default-bg);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.info-row {
|
|
279
|
+
display: flex;
|
|
280
|
+
gap: 12px;
|
|
281
|
+
font-size: 13px;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.info-label {
|
|
285
|
+
color: var(--secondary-info-fg);
|
|
286
|
+
min-width: 100px;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.info-val { color: var(--card-article-title); }
|
|
290
|
+
|
|
291
|
+
.btn {
|
|
292
|
+
align-self: flex-start;
|
|
293
|
+
padding: 0 28px;
|
|
294
|
+
height: 44px;
|
|
295
|
+
font-size: 14px;
|
|
296
|
+
font-weight: 700;
|
|
297
|
+
background: var(--key-accent, var(--primary, #D297FF));
|
|
298
|
+
color: #fff;
|
|
299
|
+
border: none;
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
transition: opacity 0.15s;
|
|
302
|
+
&.disabled, &:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
303
|
+
&:not(.disabled):hover { opacity: 0.85; }
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
@keyframes pulse {
|
|
308
|
+
0%, 100% { opacity: 1; }
|
|
309
|
+
50% { opacity: 0.4; }
|
|
310
|
+
}
|
|
311
|
+
</style>
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
import { ref, computed, inject, onMounted } from "vue"
|
|
3
|
+
import { useRoute, navigateTo } from "#app"
|
|
4
|
+
import { useNuxtApp } from "#app"
|
|
5
|
+
import {
|
|
6
|
+
fetchCampaignBySlug,
|
|
7
|
+
fetchKeyPlatforms,
|
|
8
|
+
fetchKeyRegions,
|
|
9
|
+
requestKey,
|
|
10
|
+
} from "../../services/campaignService"
|
|
11
|
+
|
|
12
|
+
const route = useRoute()
|
|
13
|
+
const { $i18n } = useNuxtApp()
|
|
14
|
+
const locale = $i18n.locale
|
|
15
|
+
|
|
16
|
+
const currentUser = inject<any>("currentUser", null)
|
|
17
|
+
const loginUrl = inject<string>("loginUrl", "/")
|
|
18
|
+
|
|
19
|
+
const keyId = Number(route.query.keyId)
|
|
20
|
+
|
|
21
|
+
// Redirect away if no keyId
|
|
22
|
+
if (!keyId) {
|
|
23
|
+
navigateTo(`/${locale.value}/key-campaigns`)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const campaign = ref<any>(null)
|
|
27
|
+
const platforms = ref<any[]>([])
|
|
28
|
+
const regions = ref<any[]>([])
|
|
29
|
+
const loading = ref(true)
|
|
30
|
+
const submitting = ref(false)
|
|
31
|
+
const error = ref("")
|
|
32
|
+
const success = ref(false)
|
|
33
|
+
const requestError = ref("")
|
|
34
|
+
|
|
35
|
+
const selectedPlatformId = ref<number | null>(null)
|
|
36
|
+
const selectedRegionId = ref<number | null>(null)
|
|
37
|
+
const description = ref("")
|
|
38
|
+
|
|
39
|
+
onMounted(async () => {
|
|
40
|
+
if (!currentUser) {
|
|
41
|
+
const returnTo = typeof window !== "undefined" ? window.location.href : ""
|
|
42
|
+
navigateTo(`${loginUrl}?redirect_to=${encodeURIComponent(returnTo)}`, { external: true })
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const [campaignRes, platformsRes, regionsRes] = await Promise.all([
|
|
47
|
+
fetchCampaignBySlug(String(keyId)),
|
|
48
|
+
fetchKeyPlatforms(keyId),
|
|
49
|
+
fetchKeyRegions(keyId),
|
|
50
|
+
])
|
|
51
|
+
campaign.value = campaignRes?.data?.data || campaignRes?.data || null
|
|
52
|
+
platforms.value = platformsRes?.data?.data || platformsRes?.data || []
|
|
53
|
+
regions.value = regionsRes?.data?.data || regionsRes?.data || []
|
|
54
|
+
|
|
55
|
+
// Pre-select if only one option
|
|
56
|
+
if (platforms.value.length === 1) selectedPlatformId.value = platforms.value[0].id
|
|
57
|
+
if (regions.value.length === 1) selectedRegionId.value = regions.value[0].id
|
|
58
|
+
} catch {
|
|
59
|
+
error.value = $i18n.t("keys.campaigns.error_loading")
|
|
60
|
+
} finally {
|
|
61
|
+
loading.value = false
|
|
62
|
+
}
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const canSubmit = computed(() =>
|
|
66
|
+
!!selectedPlatformId.value && !!selectedRegionId.value && !submitting.value,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
async function submit() {
|
|
70
|
+
if (!canSubmit.value) return
|
|
71
|
+
submitting.value = true
|
|
72
|
+
requestError.value = ""
|
|
73
|
+
try {
|
|
74
|
+
await requestKey({
|
|
75
|
+
key_id: keyId,
|
|
76
|
+
platform_ids: [selectedPlatformId.value],
|
|
77
|
+
region_ids: [selectedRegionId.value],
|
|
78
|
+
description: description.value || "-",
|
|
79
|
+
quantity_keys: 1,
|
|
80
|
+
})
|
|
81
|
+
success.value = true
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
navigateTo(`/${locale.value}/key-campaigns`)
|
|
84
|
+
}, 2000)
|
|
85
|
+
} catch (e: any) {
|
|
86
|
+
const msg = e?.response?.data?.message || e?.message || ""
|
|
87
|
+
requestError.value = msg || $i18n.t("keys.campaigns.error_request")
|
|
88
|
+
} finally {
|
|
89
|
+
submitting.value = false
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function goBack() {
|
|
94
|
+
if (typeof window !== "undefined" && window.history.length > 1) {
|
|
95
|
+
window.history.back()
|
|
96
|
+
} else {
|
|
97
|
+
navigateTo(`/${locale.value}/key-campaigns`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<template>
|
|
103
|
+
<div id="shared-redeem-key">
|
|
104
|
+
<div class="container">
|
|
105
|
+
<div class="header">
|
|
106
|
+
<NuxtLink @click.prevent="goBack" class="back">
|
|
107
|
+
<MGIcon size="2x" icon="chevron-left" />
|
|
108
|
+
<span>{{ $t("keys.campaigns.back") }}</span>
|
|
109
|
+
</NuxtLink>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<div v-if="loading" class="state">
|
|
113
|
+
<div class="skeleton" v-for="i in 3" :key="i" />
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div v-else-if="error" class="state error">{{ error }}</div>
|
|
117
|
+
|
|
118
|
+
<div v-else-if="success" class="state success">
|
|
119
|
+
<MGIcon icon="check-circle" size="3x" />
|
|
120
|
+
<p>{{ $t("keys.campaigns.request_sent") }}</p>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<div v-else class="form-content">
|
|
124
|
+
<div class="title-bar">
|
|
125
|
+
<h3>{{ $t("keys.campaigns.redeem_title") }}</h3>
|
|
126
|
+
<p v-if="campaign?.name" class="campaign-name">{{ campaign.name }}</p>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<form class="form" @submit.prevent="submit">
|
|
130
|
+
<!-- Platform -->
|
|
131
|
+
<div class="field">
|
|
132
|
+
<label class="label">{{ $t("keys.campaigns.platform") }}</label>
|
|
133
|
+
<div class="options">
|
|
134
|
+
<button
|
|
135
|
+
v-for="p in platforms"
|
|
136
|
+
:key="p.id"
|
|
137
|
+
type="button"
|
|
138
|
+
class="option"
|
|
139
|
+
:class="{ selected: selectedPlatformId === p.id }"
|
|
140
|
+
@click="selectedPlatformId = p.id"
|
|
141
|
+
>
|
|
142
|
+
{{ p.name }}
|
|
143
|
+
</button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<!-- Region -->
|
|
148
|
+
<div class="field">
|
|
149
|
+
<label class="label">{{ $t("keys.campaigns.region") }}</label>
|
|
150
|
+
<div class="options">
|
|
151
|
+
<button
|
|
152
|
+
v-for="r in regions"
|
|
153
|
+
:key="r.id"
|
|
154
|
+
type="button"
|
|
155
|
+
class="option"
|
|
156
|
+
:class="{ selected: selectedRegionId === r.id }"
|
|
157
|
+
@click="selectedRegionId = r.id"
|
|
158
|
+
>
|
|
159
|
+
{{ r.name }}
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<!-- Description (optional) -->
|
|
165
|
+
<div class="field">
|
|
166
|
+
<label class="label">{{ $t("keys.campaigns.description_optional") }}</label>
|
|
167
|
+
<textarea
|
|
168
|
+
v-model="description"
|
|
169
|
+
class="textarea"
|
|
170
|
+
:placeholder="$t('keys.campaigns.description_placeholder')"
|
|
171
|
+
rows="3"
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div v-if="requestError" class="req-error">{{ requestError }}</div>
|
|
176
|
+
|
|
177
|
+
<button
|
|
178
|
+
type="submit"
|
|
179
|
+
class="btn"
|
|
180
|
+
:class="{ disabled: !canSubmit }"
|
|
181
|
+
:disabled="!canSubmit"
|
|
182
|
+
>
|
|
183
|
+
<span v-if="submitting">{{ $t("keys.campaigns.sending") }}</span>
|
|
184
|
+
<span v-else>{{ $t("keys.campaigns.campaign.redeem") }}</span>
|
|
185
|
+
</button>
|
|
186
|
+
</form>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</template>
|
|
191
|
+
|
|
192
|
+
<style lang="scss" scoped>
|
|
193
|
+
#shared-redeem-key {
|
|
194
|
+
align-items: center;
|
|
195
|
+
width: 100%;
|
|
196
|
+
justify-content: center;
|
|
197
|
+
overflow: auto;
|
|
198
|
+
height: 100%;
|
|
199
|
+
|
|
200
|
+
.container { max-width: 680px !important; padding: 32px 16px 64px; }
|
|
201
|
+
|
|
202
|
+
.header {
|
|
203
|
+
margin-bottom: 24px;
|
|
204
|
+
.back {
|
|
205
|
+
display: inline-flex;
|
|
206
|
+
align-items: center;
|
|
207
|
+
gap: 8px;
|
|
208
|
+
font-size: 14px;
|
|
209
|
+
color: var(--secondary-info-fg);
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
text-decoration: none;
|
|
212
|
+
&:hover { color: var(--card-article-title); }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.state {
|
|
217
|
+
display: flex;
|
|
218
|
+
flex-direction: column;
|
|
219
|
+
align-items: center;
|
|
220
|
+
gap: 12px;
|
|
221
|
+
padding: 64px 0;
|
|
222
|
+
text-align: center;
|
|
223
|
+
color: var(--secondary-info-fg);
|
|
224
|
+
|
|
225
|
+
&.error { color: #ff4800; }
|
|
226
|
+
&.success { color: #4caf50; font-size: 16px; }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.skeleton {
|
|
230
|
+
width: 100%;
|
|
231
|
+
height: 48px;
|
|
232
|
+
background: var(--bg-app-badge);
|
|
233
|
+
animation: pulse 1.4s ease-in-out infinite;
|
|
234
|
+
& + & { margin-top: 8px; }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.title-bar {
|
|
238
|
+
margin-bottom: 28px;
|
|
239
|
+
h3 { font-size: 20px; font-weight: 700; color: var(--card-article-title); margin: 0 0 6px; }
|
|
240
|
+
.campaign-name { font-size: 13px; color: var(--secondary-info-fg); margin: 0; }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.form { display: flex; flex-direction: column; gap: 24px; }
|
|
244
|
+
|
|
245
|
+
.field {
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-direction: column;
|
|
248
|
+
gap: 10px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.label {
|
|
252
|
+
font-size: 13px;
|
|
253
|
+
font-weight: 600;
|
|
254
|
+
color: var(--card-article-title);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.options {
|
|
258
|
+
display: flex;
|
|
259
|
+
flex-wrap: wrap;
|
|
260
|
+
gap: 8px;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.option {
|
|
264
|
+
padding: 8px 16px;
|
|
265
|
+
font-size: 13px;
|
|
266
|
+
font-weight: 500;
|
|
267
|
+
border: 1px solid var(--button-secondary-default-bg);
|
|
268
|
+
background: transparent;
|
|
269
|
+
color: var(--secondary-info-fg);
|
|
270
|
+
cursor: pointer;
|
|
271
|
+
transition: border-color 0.15s, color 0.15s;
|
|
272
|
+
|
|
273
|
+
&:hover { border-color: var(--key-accent, var(--primary, #D297FF)); color: var(--card-article-title); }
|
|
274
|
+
&.selected {
|
|
275
|
+
border-color: var(--key-accent, var(--primary, #D297FF));
|
|
276
|
+
color: var(--key-accent, var(--primary, #D297FF));
|
|
277
|
+
background: rgba(210, 151, 255, 0.08);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.textarea {
|
|
282
|
+
width: 100%;
|
|
283
|
+
background: var(--bg-app-badge);
|
|
284
|
+
border: 1px solid var(--button-secondary-default-bg);
|
|
285
|
+
color: var(--card-article-title);
|
|
286
|
+
font-size: 13px;
|
|
287
|
+
padding: 10px 12px;
|
|
288
|
+
resize: vertical;
|
|
289
|
+
outline: none;
|
|
290
|
+
font-family: inherit;
|
|
291
|
+
&:focus { border-color: var(--key-accent, var(--primary, #D297FF)); }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.req-error {
|
|
295
|
+
font-size: 13px;
|
|
296
|
+
color: #ff4800;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.btn {
|
|
300
|
+
align-self: flex-start;
|
|
301
|
+
padding: 0 28px;
|
|
302
|
+
height: 44px;
|
|
303
|
+
font-size: 14px;
|
|
304
|
+
font-weight: 700;
|
|
305
|
+
background: var(--key-accent, var(--primary, #D297FF));
|
|
306
|
+
color: #fff;
|
|
307
|
+
border: none;
|
|
308
|
+
cursor: pointer;
|
|
309
|
+
transition: opacity 0.15s;
|
|
310
|
+
|
|
311
|
+
&.disabled, &:disabled { opacity: 0.45; cursor: not-allowed; }
|
|
312
|
+
&:not(.disabled):hover { opacity: 0.85; }
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
@keyframes pulse {
|
|
317
|
+
0%, 100% { opacity: 1; }
|
|
318
|
+
50% { opacity: 0.4; }
|
|
319
|
+
}
|
|
320
|
+
</style>
|
|
@@ -8,14 +8,21 @@ import axios, { type AxiosInstance } from "axios"
|
|
|
8
8
|
let _mainClient: AxiosInstance | null = null
|
|
9
9
|
let _authClient: AxiosInstance | null = null
|
|
10
10
|
|
|
11
|
+
// Strip /api/v1 suffix so service paths (/api/v1/...) don't double up when the
|
|
12
|
+
// env var already contains it (e.g. VITE_BASE_URL_NETWORK=https://host/api/v1).
|
|
13
|
+
function normalizeBaseUrl(url: string): string {
|
|
14
|
+
return url.replace(/\/api\/v1\/?$/, "")
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
function getMainClient(): AxiosInstance {
|
|
12
18
|
if (_mainClient) return _mainClient
|
|
13
19
|
|
|
14
|
-
const baseURL =
|
|
20
|
+
const baseURL = normalizeBaseUrl(
|
|
15
21
|
import.meta.env.VITE_CAMPAIGN_API_URL ||
|
|
16
22
|
import.meta.env.VITE_BASE_URL_NETWORK ||
|
|
17
23
|
import.meta.env.VITE_API_BASE_URL ||
|
|
18
|
-
""
|
|
24
|
+
"",
|
|
25
|
+
)
|
|
19
26
|
|
|
20
27
|
_mainClient = axios.create({ baseURL })
|
|
21
28
|
|
|
@@ -41,11 +48,12 @@ function getMainClient(): AxiosInstance {
|
|
|
41
48
|
function getAuthClient(): AxiosInstance {
|
|
42
49
|
if (_authClient) return _authClient
|
|
43
50
|
|
|
44
|
-
const baseURL =
|
|
51
|
+
const baseURL = normalizeBaseUrl(
|
|
45
52
|
import.meta.env.VITE_CAMPAIGN_API_URL ||
|
|
46
53
|
import.meta.env.VITE_BASE_URL_NETWORK ||
|
|
47
54
|
import.meta.env.VITE_API_BASE_URL ||
|
|
48
|
-
""
|
|
55
|
+
"",
|
|
56
|
+
)
|
|
49
57
|
|
|
50
58
|
_authClient = axios.create({ baseURL, withCredentials: true })
|
|
51
59
|
|