@mundogamernetwork/shared-ui 1.8.2 → 1.8.4
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/KeyCampaignDashboardCard.vue +14 -0
- package/components/keys/KeyCampaignsCarousel.vue +13 -95
- package/components/ui/MgHeaderUIUser.vue +1 -1
- package/components/ui/MgWalletPlanBadge.vue +1 -1
- package/package.json +1 -1
- package/pages/key-campaign/[slug].vue +8 -2
- package/pages/key-campaigns/[slug].vue +16 -4
- package/pages/key-campaigns/index.vue +18 -6
- package/pages/media-kit/[slug].vue +643 -610
- package/pages/presskit/game/[slug].vue +8 -2
- package/public/imgs/default-avatar.png +0 -0
- package/services/campaignService.ts +11 -8
- package/services/mediaKitProposalService.ts +35 -0
- package/stores/shortlist.ts +113 -0
|
@@ -7,6 +7,8 @@ const props = defineProps<{
|
|
|
7
7
|
slug: string;
|
|
8
8
|
name: string;
|
|
9
9
|
cover: string | null;
|
|
10
|
+
cover_focal_x?: number | null;
|
|
11
|
+
cover_focal_y?: number | null;
|
|
10
12
|
game_cover?: string | null;
|
|
11
13
|
available_count: number;
|
|
12
14
|
is_exclusive?: boolean;
|
|
@@ -34,6 +36,17 @@ const cardCover = computed(() => {
|
|
|
34
36
|
return c.cover || c.cover_src || c.game?.url_cover_src || c.game_cover || "/imgs/no-cover-img.jpg";
|
|
35
37
|
});
|
|
36
38
|
|
|
39
|
+
// Only apply the campaign's own focal point to its own cover — a game-cover/no-cover
|
|
40
|
+
// fallback wasn't framed against that point and would crop wrong if we reused it.
|
|
41
|
+
const cardCoverPosition = computed(() => {
|
|
42
|
+
const c = props.card as any;
|
|
43
|
+
const hasOwnCover = Boolean(c.cover || c.cover_src);
|
|
44
|
+
if (!hasOwnCover) return "50% 50%";
|
|
45
|
+
const fx = c.cover_focal_x ?? 0.5;
|
|
46
|
+
const fy = c.cover_focal_y ?? 0.5;
|
|
47
|
+
return `${fx * 100}% ${fy * 100}%`;
|
|
48
|
+
});
|
|
49
|
+
|
|
37
50
|
const userCanAccess = computed(() => props.publicMode || props.card.user_can_access !== false);
|
|
38
51
|
const isExpired = computed(() => props.card.isExpired ?? false);
|
|
39
52
|
const isUpcoming = computed(() => props.card.isUpcoming ?? false);
|
|
@@ -99,6 +112,7 @@ const detailRoute = computed(() =>
|
|
|
99
112
|
<img
|
|
100
113
|
:src="cardCover"
|
|
101
114
|
:alt="card.name"
|
|
115
|
+
:style="{ objectPosition: cardCoverPosition }"
|
|
102
116
|
:class="{ 'gray-out': isExpired || isUpcoming || !userCanAccess }"
|
|
103
117
|
@error="(e: Event) => { (e.target as HTMLImageElement).src = '/imgs/no-cover-img.jpg' }"
|
|
104
118
|
/>
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
<script lang="ts" setup>
|
|
2
2
|
import { ref, onMounted, inject } from "vue"
|
|
3
3
|
import { fetchCampaigns } from "../../services/campaignService"
|
|
4
|
+
import KeyCampaignDashboardCard from "./KeyCampaignDashboardCard.vue"
|
|
4
5
|
|
|
5
6
|
const props = defineProps<{
|
|
6
7
|
excludeId?: number | null
|
|
7
8
|
mgPlatform?: string
|
|
9
|
+
publicMode?: boolean
|
|
10
|
+
baseRoute?: string
|
|
8
11
|
}>()
|
|
9
12
|
|
|
10
13
|
const mgPlatform = props.mgPlatform || (inject<string>("mgPlatform", "MGTV"))
|
|
@@ -46,31 +49,13 @@ onMounted(async () => {
|
|
|
46
49
|
</div>
|
|
47
50
|
|
|
48
51
|
<div v-else class="kc-carousel__track">
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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>
|
|
52
|
+
<div v-for="item in items" :key="item.id" class="kc-carousel__card">
|
|
53
|
+
<KeyCampaignDashboardCard
|
|
54
|
+
:card="{ ...item, isExpired: item.is_open === false }"
|
|
55
|
+
:public-mode="publicMode"
|
|
56
|
+
:base-route="baseRoute ?? 'key-campaigns'"
|
|
57
|
+
/>
|
|
58
|
+
</div>
|
|
74
59
|
</div>
|
|
75
60
|
</div>
|
|
76
61
|
</template>
|
|
@@ -105,81 +90,14 @@ onMounted(async () => {
|
|
|
105
90
|
}
|
|
106
91
|
|
|
107
92
|
&__skeleton {
|
|
108
|
-
flex: 0 0
|
|
109
|
-
height:
|
|
93
|
+
flex: 0 0 200px;
|
|
94
|
+
height: 280px;
|
|
110
95
|
background: var(--bg-app-badge);
|
|
111
96
|
animation: pulse 1.4s ease-in-out infinite;
|
|
112
97
|
}
|
|
113
98
|
|
|
114
99
|
&__card {
|
|
115
|
-
flex: 0 0
|
|
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: var(--overlay-dark, rgba(0, 0, 0, 0.55));
|
|
157
|
-
font-size: 11px;
|
|
158
|
-
font-weight: 700;
|
|
159
|
-
color: var(--card-article-title, #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);
|
|
100
|
+
flex: 0 0 200px;
|
|
183
101
|
}
|
|
184
102
|
}
|
|
185
103
|
|
|
@@ -107,7 +107,7 @@ const handleLogout = async () => {
|
|
|
107
107
|
<div v-if="features.wallet && user?.coins !== null" class="row info-row">
|
|
108
108
|
<span class="coins">
|
|
109
109
|
<MGIcon icon="coin" />
|
|
110
|
-
{{ user?.coins ?? 0 }}
|
|
110
|
+
{{ Math.floor(user?.coins ?? 0).toLocaleString() }}
|
|
111
111
|
</span>
|
|
112
112
|
</div>
|
|
113
113
|
|
|
@@ -44,7 +44,7 @@ const isSubscriber = computed(() => !!(user.value as any)?.subscription?.is_subs
|
|
|
44
44
|
<div class="mg-wallet-plan">
|
|
45
45
|
<span v-if="showWallet" class="mg-wallet-plan__coins">
|
|
46
46
|
<MGIcon icon="coin" />
|
|
47
|
-
{{ walletBalance.toLocaleString() }}
|
|
47
|
+
{{ Math.floor(walletBalance).toLocaleString() }}
|
|
48
48
|
<span class="mg-wallet-plan__currency">MGC</span>
|
|
49
49
|
</span>
|
|
50
50
|
<span
|
package/package.json
CHANGED
|
@@ -78,7 +78,13 @@ const restriction = computed(() => {
|
|
|
78
78
|
|
|
79
79
|
<template v-else>
|
|
80
80
|
<div class="kc__hero">
|
|
81
|
-
<img
|
|
81
|
+
<img
|
|
82
|
+
v-if="key.cover_src"
|
|
83
|
+
:src="key.cover_src"
|
|
84
|
+
:alt="key.name"
|
|
85
|
+
class="kc__cover"
|
|
86
|
+
:style="{ objectPosition: `${(key.cover_focal_x ?? 0.5) * 100}% ${(key.cover_focal_y ?? 0.5) * 100}%` }"
|
|
87
|
+
>
|
|
82
88
|
<div class="kc__hero-body">
|
|
83
89
|
<h1 class="kc__title">{{ key.name }}</h1>
|
|
84
90
|
<div v-if="restriction" class="kc__badge">🔒 {{ restriction }}</div>
|
|
@@ -96,7 +102,7 @@ const restriction = computed(() => {
|
|
|
96
102
|
/>
|
|
97
103
|
</div>
|
|
98
104
|
|
|
99
|
-
<KeyCampaignsCarousel :exclude-id="key.id ?? null" />
|
|
105
|
+
<KeyCampaignsCarousel :exclude-id="key.id ?? null" :public-mode="true" base-route="key-campaign" />
|
|
100
106
|
</template>
|
|
101
107
|
</div>
|
|
102
108
|
</template>
|
|
@@ -840,9 +840,9 @@ onMounted(async () => {
|
|
|
840
840
|
<img
|
|
841
841
|
v-for="(u, index) in redeemedUsers?.users?.slice(0, 4)"
|
|
842
842
|
:key="index"
|
|
843
|
-
:src="u.avatar_url || '/imgs/avatar
|
|
843
|
+
:src="u.avatar_url || '/imgs/default-avatar.png'"
|
|
844
844
|
:alt="u.nickname"
|
|
845
|
-
@error="(e: Event) => { const t = e.target as HTMLImageElement; if (t
|
|
845
|
+
@error="(e: Event) => { const t = e.target as HTMLImageElement; if (t) { t.onerror = null; t.src = '/imgs/default-avatar.png' } }"
|
|
846
846
|
/>
|
|
847
847
|
</div>
|
|
848
848
|
<div class="text">
|
|
@@ -872,9 +872,9 @@ onMounted(async () => {
|
|
|
872
872
|
</div>
|
|
873
873
|
<div v-for="(u, index) in redeemedUsers?.users" :key="index" class="users-modal-row">
|
|
874
874
|
<img
|
|
875
|
-
:src="u.avatar_url || '/imgs/avatar
|
|
875
|
+
:src="u.avatar_url || '/imgs/default-avatar.png'"
|
|
876
876
|
:alt="u.nickname"
|
|
877
|
-
@error="(e: Event) => { const t = e.target as HTMLImageElement; if (t
|
|
877
|
+
@error="(e: Event) => { const t = e.target as HTMLImageElement; if (t) { t.onerror = null; t.src = '/imgs/default-avatar.png' } }"
|
|
878
878
|
/>
|
|
879
879
|
<span>{{ u.nickname || `${u.first_name || ''} ${u.last_name || ''}`.trim() }}</span>
|
|
880
880
|
</div>
|
|
@@ -926,6 +926,8 @@ onMounted(async () => {
|
|
|
926
926
|
<KeyCampaignsCarousel
|
|
927
927
|
:exclude-id="campaign?.id ?? null"
|
|
928
928
|
:mg-platform="mgPlatform"
|
|
929
|
+
:public-mode="true"
|
|
930
|
+
base-route="key-campaigns"
|
|
929
931
|
class="container"
|
|
930
932
|
/>
|
|
931
933
|
</div>
|
|
@@ -1482,6 +1484,16 @@ onMounted(async () => {
|
|
|
1482
1484
|
.time { gap: 6px; .card-time { height: auto; padding: 8px; font-size: 24px; } }
|
|
1483
1485
|
.actions .data { flex-direction: column; }
|
|
1484
1486
|
.status { flex-direction: column; align-items: start; gap: 8px; }
|
|
1487
|
+
.requirements .tags {
|
|
1488
|
+
flex-wrap: wrap;
|
|
1489
|
+
.tag {
|
|
1490
|
+
height: auto;
|
|
1491
|
+
min-height: 32px;
|
|
1492
|
+
padding: 8px;
|
|
1493
|
+
flex: 1 1 calc(50% - 6px);
|
|
1494
|
+
text-align: center;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1485
1497
|
}
|
|
1486
1498
|
}
|
|
1487
1499
|
}
|
|
@@ -1090,12 +1090,24 @@ onUnmounted(() => {
|
|
|
1090
1090
|
gap: 12px;
|
|
1091
1091
|
button { height: 44px; }
|
|
1092
1092
|
|
|
1093
|
-
// FormAppSelect ships at 32px
|
|
1094
|
-
//
|
|
1095
|
-
//
|
|
1096
|
-
//
|
|
1097
|
-
|
|
1098
|
-
|
|
1093
|
+
// FormAppSelect ships at 32px with its own light --input-bg
|
|
1094
|
+
// (its default look on other pages, e.g. account settings
|
|
1095
|
+
// forms on a card background) — bump to match the 44px
|
|
1096
|
+
// filter pills/search box height, and repaint it to match
|
|
1097
|
+
// the dark toolbar instead of the light default, so it
|
|
1098
|
+
// reads as one consistent row instead of a native-looking
|
|
1099
|
+
// light select stranded next to dark pills.
|
|
1100
|
+
:deep(.select-wrapper) {
|
|
1101
|
+
display: flex;
|
|
1102
|
+
align-items: center;
|
|
1103
|
+
height: 44px;
|
|
1104
|
+
border-bottom-color: var(--secondary-info-fg);
|
|
1105
|
+
}
|
|
1106
|
+
:deep(.select-wrapper select) {
|
|
1107
|
+
height: 44px;
|
|
1108
|
+
background: transparent;
|
|
1109
|
+
color: var(--secondary-info-fg);
|
|
1110
|
+
}
|
|
1099
1111
|
}
|
|
1100
1112
|
|
|
1101
1113
|
.header-left .btn.tertiary.active {
|