@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.
@@ -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
- <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>
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 180px;
109
- height: 140px;
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 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: 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.8.2",
3
+ "version": "1.8.4",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -78,7 +78,13 @@ const restriction = computed(() => {
78
78
 
79
79
  <template v-else>
80
80
  <div class="kc__hero">
81
- <img v-if="key.cover_src" :src="key.cover_src" :alt="key.name" class="kc__cover">
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-default.jpg'"
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 && t.src !== '/imgs/avatar-default.jpg') t.src = '/imgs/avatar-default.jpg' }"
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-default.jpg'"
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 && t.src !== '/imgs/avatar-default.jpg') t.src = '/imgs/avatar-default.jpg' }"
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 (its own default look on other
1094
- // pages) bump to match the 44px filter pills/search box so
1095
- // the toolbar reads as one consistent row instead of controls
1096
- // bobbing at two different heights.
1097
- :deep(.select-wrapper) { display: flex; align-items: center; height: 44px; }
1098
- :deep(.select-wrapper select) { height: 44px; }
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 {