@mundogamernetwork/shared-ui 1.8.17 → 1.8.18

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.
@@ -12,7 +12,7 @@
12
12
  <!-- Bundle Form -->
13
13
  <div v-if="showForm" class="bundle-form">
14
14
  <div class="visibility-note">
15
- <MGIcon icon="info" />
15
+ <MGIcon icon="about" />
16
16
  <span>{{ $t("media_kit.bundles.visibility_note") }}</span>
17
17
  </div>
18
18
  <div class="form-group">
@@ -47,7 +47,7 @@
47
47
  <span class="card-price">{{ formatPrice(card.price_min, card.currency) }}</span>
48
48
  </div>
49
49
  <div class="check-indicator">
50
- <MGIcon icon="checkmark" v-if="form.rate_card_ids.includes(card.id)" />
50
+ <MGIcon icon="verified" v-if="form.rate_card_ids.includes(card.id)" />
51
51
  </div>
52
52
  <div v-if="form.rate_card_ids.includes(card.id)" class="card-qty" @click.stop.prevent>
53
53
  <span>×</span>
@@ -24,11 +24,11 @@
24
24
  </select>
25
25
  </div>
26
26
  <div class="form-group">
27
- <label>{{ $t("media_kit.editor.platform") }}</label>
28
- <select v-model="form.platform_id" :class="{ 'input-error': rateCardErrors.platform_id }" @change="delete rateCardErrors.platform_id" required>
27
+ <label>{{ $t("media_kit.editor.ad_channel") }}</label>
28
+ <select v-model="form.ad_channel_id" :class="{ 'input-error': rateCardErrors.ad_channel_id }" @change="delete rateCardErrors.ad_channel_id" required>
29
29
  <option value="">{{ $t("media_kit.editor.select") }}</option>
30
- <option v-for="platform in platforms" :key="platform.id" :value="platform.id">
31
- {{ platform.name }}
30
+ <option v-for="channel in adChannels" :key="channel.id" :value="channel.id">
31
+ {{ channel.name }}
32
32
  </option>
33
33
  </select>
34
34
  </div>
@@ -110,7 +110,7 @@
110
110
  <div v-for="card in rateCards" :key="card.id" class="rate-card-item">
111
111
  <div class="card-info">
112
112
  <span class="format">{{ card.content_format?.name || card.label }}</span>
113
- <span class="channel">{{ card.platform?.name }}</span>
113
+ <span class="channel">{{ card.ad_channel?.name }}</span>
114
114
  <span class="price">{{ formatPrice(card.price_min, card.price_max, card.currency) }}</span>
115
115
  </div>
116
116
  <div class="card-actions">
@@ -134,7 +134,7 @@ const { t } = useI18n();
134
134
  interface RateCard {
135
135
  id: number;
136
136
  content_format_id: number;
137
- platform_id: number;
137
+ ad_channel_id: number;
138
138
  label?: string;
139
139
  price_min: string;
140
140
  price_max?: string;
@@ -143,7 +143,7 @@ interface RateCard {
143
143
  description?: string;
144
144
  is_active: boolean;
145
145
  content_format?: { id: number; name: string };
146
- platform?: { id: number; name: string };
146
+ ad_channel?: { id: number; name: string };
147
147
  }
148
148
 
149
149
  interface LookupItem {
@@ -156,7 +156,7 @@ const props = withDefaults(
156
156
  rateCards: RateCard[];
157
157
  influencerProInfoId: number;
158
158
  contentFormats: LookupItem[];
159
- platforms: LookupItem[];
159
+ adChannels: LookupItem[];
160
160
  isEnabled?: boolean;
161
161
  /** MGN commission % for the current plan (host app resolves this, e.g. from feature_limits). */
162
162
  commissionPercent?: number;
@@ -196,7 +196,7 @@ const saving = ref(false);
196
196
 
197
197
  const defaultForm = {
198
198
  content_format_id: '',
199
- platform_id: '',
199
+ ad_channel_id: '',
200
200
  price_min: '',
201
201
  price_max: '',
202
202
  currency: 'USD',
@@ -265,7 +265,7 @@ const editCard = (card: RateCard) => {
265
265
  editing.value = card.id;
266
266
  form.value = {
267
267
  content_format_id: String(card.content_format_id),
268
- platform_id: String(card.platform_id),
268
+ ad_channel_id: String(card.ad_channel_id),
269
269
  price_min: card.price_min,
270
270
  price_max: card.price_max || '',
271
271
  currency: card.currency,
@@ -283,7 +283,7 @@ const saveRateCard = async () => {
283
283
  // Client-side required field check (HTML required bypassed by @click)
284
284
  Object.keys(rateCardErrors).forEach(k => delete rateCardErrors[k]);
285
285
  if (!form.value.content_format_id) rateCardErrors.content_format_id = 'required';
286
- if (!form.value.platform_id) rateCardErrors.platform_id = 'required';
286
+ if (!form.value.ad_channel_id) rateCardErrors.ad_channel_id = 'required';
287
287
  if (!form.value.price_min && form.value.price_min !== 0) rateCardErrors.price_min = 'required';
288
288
  if (Object.keys(rateCardErrors).length) return;
289
289
 
@@ -293,7 +293,7 @@ const saveRateCard = async () => {
293
293
  ...form.value,
294
294
  influencer_professional_info_id: props.influencerProInfoId,
295
295
  content_format_id: Number(form.value.content_format_id),
296
- platform_id: Number(form.value.platform_id),
296
+ ad_channel_id: Number(form.value.ad_channel_id),
297
297
  price_min: Number(form.value.price_min),
298
298
  price_max: form.value.price_max ? Number(form.value.price_max) : null,
299
299
  delivery_time_days: form.value.delivery_time_days ? Number(form.value.delivery_time_days) : null,
@@ -80,7 +80,7 @@ function downloadAll() {
80
80
  <div class="gallery-header">
81
81
  <h3 class="section-title">{{ $t('press_kit.assets') }}</h3>
82
82
  <button v-if="allowDownload && assets.length" class="btn-download-all" @click="downloadAll">
83
- <MGIcon icon="download" /> {{ $t('press_kit.download_all') }}
83
+ <MGIcon icon="save" /> {{ $t('press_kit.download_all') }}
84
84
  </button>
85
85
  </div>
86
86
 
@@ -134,7 +134,7 @@ function downloadAll() {
134
134
  <div class="lb-info">
135
135
  <span>{{ imageAssets[lightboxIndex]?.title || imageAssets[lightboxIndex]?.type }}</span>
136
136
  <button v-if="allowDownload" class="lb-download" @click="downloadAsset(imageAssets[lightboxIndex])">
137
- <MGIcon icon="download" /> {{ $t('press_kit.download') }}
137
+ <MGIcon icon="save" /> {{ $t('press_kit.download') }}
138
138
  </button>
139
139
  </div>
140
140
  </div>
@@ -27,7 +27,7 @@ defineProps<Props>();
27
27
  rel="noopener"
28
28
  :class="['award-card', { 'no-link': !award.url }]"
29
29
  >
30
- <MGIcon icon="trophy" class="award-icon" />
30
+ <MGIcon icon="achievements" class="award-icon" />
31
31
  <div class="award-info">
32
32
  <span class="award-title">{{ award.title }}</span>
33
33
  <span v-if="award.event || award.year" class="award-event">
@@ -27,7 +27,7 @@ const youtubeEmbedUrl = computed(() => {
27
27
  <h1 v-else class="hero-title">{{ name }}</h1>
28
28
  <p v-if="tagline" class="hero-tagline">{{ tagline }}</p>
29
29
  <button v-if="trailerUrl" class="btn-trailer" @click="showTrailer = true">
30
- <MGIcon icon="play" /> {{ $t('press_kit.watch_trailer') }}
30
+ <MGIcon icon="videos" /> {{ $t('press_kit.watch_trailer') }}
31
31
  </button>
32
32
  </div>
33
33
  </div>
@@ -55,7 +55,7 @@ function formatType(type?: string) {
55
55
  :class="['video-item', { active: activeVideo?.id === video.id }]"
56
56
  @click="activeVideo = video"
57
57
  >
58
- <MGIcon icon="play-circle" />
58
+ <MGIcon icon="videos" />
59
59
  <div class="video-meta">
60
60
  <span class="video-title">{{ video.title }}</span>
61
61
  <span v-if="video.type" class="video-type">{{ formatType(video.type) }}</span>
@@ -130,38 +130,38 @@ const detailRoute = computed(() =>
130
130
 
131
131
  <!-- Exclusive badge -->
132
132
  <div v-if="card.is_exclusive && !userCanAccess" class="kc-card__exclusive-badge kc-card__exclusive-lock">
133
- <i class="fa-solid fa-crown" /> {{ $t("keys.card.exclusive_upgrade") }}
133
+ <MGIcon size="1x" icon="reward" /> {{ $t("keys.card.exclusive_upgrade") }}
134
134
  </div>
135
135
  <div v-else-if="card.is_exclusive" class="kc-card__exclusive-badge">
136
- <i class="fa-solid fa-crown" /> {{ $t("keys.card.exclusive") }}
136
+ <MGIcon size="1x" icon="reward" /> {{ $t("keys.card.exclusive") }}
137
137
  </div>
138
138
 
139
139
  <!-- Tier-locked badge -->
140
140
  <div v-else-if="card.is_tier_locked && card.access_tier !== 'free' && !userCanAccess" class="kc-card__exclusive-badge">
141
- <i class="fa-solid fa-crown" /> {{ cardTierLabel(card.access_tier) }}
141
+ <MGIcon size="1x" icon="reward" /> {{ cardTierLabel(card.access_tier) }}
142
142
  </div>
143
143
 
144
144
  <!-- Early access badge -->
145
145
  <div v-else-if="(card.early_access_hours ?? 0) > 0 && userCanAccess" class="kc-card__early-access-badge">
146
- <i class="fa-solid fa-bolt" /> {{ $t("keys.card.early_access") }}
146
+ <MGIcon size="1x" icon="clock" /> {{ $t("keys.card.early_access") }}
147
147
  </div>
148
148
 
149
149
  <!-- Streamer level badge -->
150
150
  <div v-if="card.streamer_type === 2 || card.streamer_type === 3" class="kc-card__exclusive-badge kc-card__level-badge">
151
- <i class="fa-solid fa-user-check" />
151
+ <MGIcon size="1x" icon="verified" />
152
152
  {{ card.streamer_type === 2 ? $t("keys.card.level_official") : $t("keys.card.level_affiliate") }}
153
153
  </div>
154
154
 
155
155
  <!-- Early-access lock overlay -->
156
156
  <div v-if="!userCanAccess && card.available_at_for_user" class="kc-card__lock-overlay">
157
- <i class="fa-solid fa-lock" />
157
+ <MGIcon size="1x" icon="lock" />
158
158
  <span class="lock-label">{{ $t("keys.card.early_access_lock_title") }}</span>
159
159
  <span class="lock-countdown">{{ $t("keys.card.early_access_opens_in") }} {{ formatCountdown(card.available_at_for_user) }}</span>
160
160
  </div>
161
161
 
162
162
  <!-- Exclusive lock overlay -->
163
163
  <div v-else-if="!userCanAccess && card.is_exclusive" class="kc-card__lock-overlay kc-card__lock-overlay--exclusive">
164
- <i class="fa-solid fa-crown" />
164
+ <MGIcon size="1x" icon="reward" />
165
165
  <span class="lock-label">{{ $t("keys.card.exclusive_lock_title") }}</span>
166
166
  <span class="lock-cta">{{ $t("keys.card.exclusive_upgrade_cta") }}</span>
167
167
  </div>
@@ -198,7 +198,7 @@ const detailRoute = computed(() =>
198
198
  </NuxtLink>
199
199
  <NuxtLink v-if="!publicMode" :to="`/${locale}/dashboard/pricing`">
200
200
  <button class="btn secondary upgrade-btn">
201
- <i class="fa-solid fa-arrow-up" /> {{ $t("keys.card.upgrade_plan") }}
201
+ <MGIcon size="1x" icon="chevron-up" /> {{ $t("keys.card.upgrade_plan") }}
202
202
  </button>
203
203
  </NuxtLink>
204
204
  </template>
@@ -54,7 +54,7 @@ const categoryLabel = computed(() => {
54
54
  </div>
55
55
 
56
56
  <div v-if="campaign.eligible === false" class="pt-brief__ineligible">
57
- <i class="fa-solid fa-lock" />
57
+ <MGIcon size="1x" icon="lock" />
58
58
  <span>{{ campaign.ineligibility_reason || $t("playtest.brief.ineligible_generic", "You don't currently qualify for this playtest.") }}</span>
59
59
  </div>
60
60
 
@@ -23,8 +23,8 @@ const emit = defineEmits<{
23
23
  <template>
24
24
  <div class="pt-pending" :class="{ 'pt-pending--ineligible': ineligible }">
25
25
  <div class="pt-pending__icon">
26
- <i v-if="ineligible" class="fa-solid fa-circle-exclamation" />
27
- <i v-else class="fa-solid fa-circle-check" />
26
+ <MGIcon v-if="ineligible" size="1x" icon="exclamation" />
27
+ <MGIcon v-else size="1x" icon="verified-filled" />
28
28
  </div>
29
29
 
30
30
  <h3 class="pt-pending__title">
@@ -65,7 +65,7 @@ function submit() {
65
65
  :aria-label="$t('playtest.concept_poll.move_up', 'Move up')"
66
66
  @click="moveUp(index)"
67
67
  >
68
- <i class="fa-solid fa-arrow-up" />
68
+ <MGIcon size="1x" icon="chevron-up" />
69
69
  </button>
70
70
  <button
71
71
  type="button"
@@ -74,7 +74,7 @@ function submit() {
74
74
  :aria-label="$t('playtest.concept_poll.move_down', 'Move down')"
75
75
  @click="moveDown(index)"
76
76
  >
77
- <i class="fa-solid fa-arrow-down" />
77
+ <MGIcon size="1x" icon="chevron-down" />
78
78
  </button>
79
79
  </span>
80
80
  </li>
@@ -35,7 +35,7 @@ const scheduledAt = computed(() => (props.session?.scheduled_at ? new Date(props
35
35
  <p class="join-call-panel__time">{{ scheduledAt?.toLocaleString() }}</p>
36
36
 
37
37
  <div v-if="session.tester_confirmed_at" class="join-call-panel__confirmed-badge">
38
- <i class="fa-solid fa-circle-check" />
38
+ <MGIcon size="1x" icon="verified-filled" />
39
39
  {{ $t("playtest.moderated.you_confirmed", "You've confirmed attendance") }}
40
40
  </div>
41
41
 
@@ -106,7 +106,7 @@ const handleLogout = async () => {
106
106
 
107
107
  <div v-if="features.wallet && user?.coins !== null" class="row info-row">
108
108
  <span class="coins">
109
- <MGIcon icon="coin" />
109
+ <MGIcon icon="coin-v2" />
110
110
  {{ Math.floor(user?.coins ?? 0).toLocaleString() }}
111
111
  </span>
112
112
  </div>
@@ -43,7 +43,7 @@ const isSubscriber = computed(() => !!(user.value as any)?.subscription?.is_subs
43
43
  <template>
44
44
  <div class="mg-wallet-plan">
45
45
  <span v-if="showWallet" class="mg-wallet-plan__coins">
46
- <MGIcon icon="coin" />
46
+ <MGIcon icon="coin-v2" />
47
47
  {{ Math.floor(walletBalance).toLocaleString() }}
48
48
  <span class="mg-wallet-plan__currency">MGC</span>
49
49
  </span>
package/locales/de.json CHANGED
@@ -195,6 +195,8 @@
195
195
  "required_data": "Erforderliche Angaben",
196
196
  "already_requested": "Sie haben bereits eine Anfrage für diese Kampagne gestellt.",
197
197
  "sold_out": "Alle Keys für diese Kampagne wurden bereits eingelöst.",
198
+ "keys_left": "{n} Key übrig | {n} Keys übrig",
199
+ "keys_low_stock": "Letzter Key | Letzte {n}",
198
200
  "back_to_campaign": "Zur Kampagne zurückkehren",
199
201
  "status_pending": "Anfrage ausstehend — bitte warten Sie auf die Genehmigung",
200
202
  "status_approved": "Anfrage genehmigt — Sie können jetzt Ihren Schlüssel abrufen",
package/locales/en.json CHANGED
@@ -195,6 +195,8 @@
195
195
  "required_data": "Required data",
196
196
  "already_requested": "You have already submitted a request for this campaign.",
197
197
  "sold_out": "All keys for this campaign have already been claimed.",
198
+ "keys_left": "{n} key left | {n} keys left",
199
+ "keys_low_stock": "Last key | Last {n}",
198
200
  "back_to_campaign": "Back to campaign",
199
201
  "status_pending": "Request pending — waiting for approval",
200
202
  "status_approved": "Request approved — you can now retrieve your key",
package/locales/es.json CHANGED
@@ -195,6 +195,8 @@
195
195
  "required_data": "Datos requeridos",
196
196
  "already_requested": "Ya has enviado una solicitud para esta campaña.",
197
197
  "sold_out": "Todas las claves de esta campaña ya fueron reclamadas.",
198
+ "keys_left": "{n} clave restante | {n} claves restantes",
199
+ "keys_low_stock": "Última clave | Últimas {n}",
198
200
  "back_to_campaign": "Volver a la campaña",
199
201
  "status_pending": "Solicitud pendiente — esperando aprobación",
200
202
  "status_approved": "Solicitud aprobada — ya puedes obtener tu clave",
@@ -195,6 +195,8 @@
195
195
  "required_data": "Dados obrigatórios",
196
196
  "already_requested": "Você já enviou uma solicitação para esta campanha.",
197
197
  "sold_out": "Todas as chaves desta campanha já foram resgatadas.",
198
+ "keys_left": "{n} chave restante | {n} chaves restantes",
199
+ "keys_low_stock": "Última chave | Últimas {n}",
198
200
  "back_to_campaign": "Voltar para a campanha",
199
201
  "status_pending": "Solicitação pendente — aguardando aprovação",
200
202
  "status_approved": "Solicitação aprovada — você pode resgatar sua chave",
package/locales/ro.json CHANGED
@@ -195,6 +195,8 @@
195
195
  "required_data": "Date obligatorii",
196
196
  "already_requested": "Ați trimis deja o cerere pentru această campanie.",
197
197
  "sold_out": "Toate cheile pentru această campanie au fost deja revendicate.",
198
+ "keys_left": "{n} cheie rămasă | {n} chei rămase",
199
+ "keys_low_stock": "Ultima cheie | Ultimele {n}",
198
200
  "back_to_campaign": "Înapoi la campanie",
199
201
  "status_pending": "Cerere în așteptare — așteptați aprobarea",
200
202
  "status_approved": "Cerere aprobată — puteți prelua cheia",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.8.17",
3
+ "version": "1.8.18",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
package/pages/icons.vue CHANGED
@@ -1,13 +1,48 @@
1
1
  <script setup lang="ts">
2
2
  definePageMeta({ layout: "default" });
3
3
 
4
+ // The real contents of the custom-icons font (assets/scss/_icons.scss in
5
+ // each consuming app — identical across tv/community/agency).
6
+ //
7
+ // This list used to be a plausible-sounding invention: "bell", "user",
8
+ // "star", "check", "info", "download"... none of which exist in the font.
9
+ // A catalogue that lists icons you cannot use is worse than no catalogue —
10
+ // it is where `icon="check"` and friends came from, rendering an empty
11
+ // element on real screens.
4
12
  const icons = [
5
- "home", "search", "message", "bell", "gear", "user", "chevron-left",
6
- "chevron-right", "chevron-down", "chevron-up", "close", "menu",
7
- "link", "star", "heart", "share", "edit", "trash", "plus", "minus",
8
- "check", "info", "warning", "error", "calendar", "clock", "download",
9
- "upload", "filter", "sort", "grid", "list", "eye", "eye-off",
10
- "lock", "unlock", "mail", "phone", "map", "globe", "flag",
13
+ "about", "achievements", "add-community", "add-friend", "add-team", "added", "administration",
14
+ "ai", "ai-vr", "analytics-research", "animation", "arcade", "arts", "arts-crafts", "audio-sound",
15
+ "battle-royale", "beta-feature", "bug", "business-operations", "camera", "camera-solid",
16
+ "cancel-message", "cancel-notification", "card-games", "cart", "cel-v1", "cel-v2", "chevron-down",
17
+ "chevron-left", "chevron-right", "chevron-up", "childcare-voucher", "clear", "clock",
18
+ "clock-filled", "close", "coin-v1", "coin-v2", "community", "community-filled",
19
+ "community-management", "community-plus", "contact-v2", "coop", "creatormode",
20
+ "customer-services", "cycle", "dental-healthcare", "discord", "discounts", "discover",
21
+ "display-blocks", "display-list", "double-", "double-chevron-left", "e-sports", "edit",
22
+ "editorial-journalism", "education", "educational", "engineering", "event-notification",
23
+ "exclamation", "exclusive-themes", "external-link", "eye", "eye-slash", "facebook", "facebook-v2",
24
+ "faq", "feed", "fight", "filter", "flexible-work", "follow", "free-food", "gambling", "game-jam",
25
+ "game-release-notification", "game-room", "games", "games-filled", "gear", "gear-dev", "giveaway",
26
+ "gym", "hack-slash", "handshake-filled", "healthcheck", "home", "horror", "impressum",
27
+ "infinity-community", "infinity-team", "information-technology", "instagran", "interaction",
28
+ "jack-of-all-trades", "job-apply", "jolife-insurance", "language", "language-class", "legal",
29
+ "like", "like-filled", "link", "linkedin", "linkedin-v2", "list", "localisation", "location",
30
+ "location-v1", "location-v2", "lock", "log-status", "mail-v1", "mail-v2", "manage-team",
31
+ "membership", "menu", "message", "message-active", "money", "moon", "more",
32
+ "narrative-design-writting", "new-friend", "news", "no-ads", "no-data", "no-dresscode",
33
+ "not-interested", "notification", "notification-active", "office-library", "official-streamer",
34
+ "official-streamer-filled", "official-writer", "operations", "others", "paid-overtime", "partner",
35
+ "partner-filled", "phone", "picture", "plan", "plan-filled", "platformer", "politics",
36
+ "private-medic", "procurement-supply-chain", "production", "profile", "profile-filled",
37
+ "programmmig-development", "public-relations", "public-transport", "puzzlers",
38
+ "quality-assurance", "quality-control", "question", "racing", "rate", "recognition",
39
+ "recruitment", "region", "report", "retirement", "retro", "reward", "roadmap", "rpg",
40
+ "sales-representative", "save", "save-filled", "search", "senior-management", "share",
41
+ "share-filled", "shooter", "simulation", "simulator", "smile", "social-medias", "sort", "sports",
42
+ "stipend", "stocks", "strategy", "strategy-1", "stream", "suppot", "survival", "team",
43
+ "team-event", "telegran", "terms", "text-size", "theme", "tiktok", "training", "trash", "twitter",
44
+ "twitter-v2", "twtch", "ux-design", "verified", "verified-filled", "version", "videos", "vr",
45
+ "wallet", "web-development", "webpage", "whatsapp", "wiki", "youtube",
11
46
  ];
12
47
  </script>
13
48
 
@@ -127,44 +127,49 @@
127
127
  @error="(e) => { (e.target as HTMLImageElement).src = '/imgs/no-cover-img.jpg' }"
128
128
  />
129
129
 
130
+ <!-- Scarcity only. The plain count now lives in the card's
131
+ info row, where card metadata is actually read — a bare
132
+ number floating on the artwork said nothing, and the icon
133
+ that was supposed to explain it never rendered. Up here it
134
+ earns its place only when stock is running out. -->
130
135
  <div
131
136
  class="keys-badge"
132
- v-if="card.available_count > 0 && card.user_can_access && !card.isExpired && !card.isUpcoming"
137
+ v-if="showsLowStock(card)"
133
138
  :title="$t('keys.campaigns.keys_left', { n: card.available_count })"
134
139
  >
135
- <i class="fa-solid fa-key" />
136
- {{ card.available_count }}
140
+ <MGIcon size="1x" icon="giveaway" />
141
+ {{ $t("keys.campaigns.keys_low_stock", { n: card.available_count }) }}
137
142
  </div>
138
143
 
139
144
  <div
140
145
  v-if="card.is_exclusive && !card.user_can_access"
141
146
  class="exclusive-badge exclusive-lock"
142
147
  >
143
- <i class="fa-solid fa-crown" /> {{ $t("keys.campaigns.exclusive_upgrade") }}
148
+ <MGIcon size="1x" icon="reward" /> {{ $t("keys.campaigns.exclusive_upgrade") }}
144
149
  </div>
145
150
  <div v-else-if="card.is_exclusive" class="exclusive-badge">
146
- <i class="fa-solid fa-crown" /> {{ $t("keys.campaigns.exclusive") }}
151
+ <MGIcon size="1x" icon="reward" /> {{ $t("keys.campaigns.exclusive") }}
147
152
  </div>
148
153
 
149
154
  <div
150
155
  v-else-if="card.is_tier_locked && card.access_tier !== 'free' && !card.user_can_access"
151
156
  class="exclusive-badge"
152
157
  >
153
- <i class="fa-solid fa-crown" /> {{ cardTierLabel(card.access_tier) }}
158
+ <MGIcon size="1x" icon="reward" /> {{ cardTierLabel(card.access_tier) }}
154
159
  </div>
155
160
 
156
161
  <div
157
162
  v-else-if="card.early_access_hours > 0 && card.user_can_access"
158
163
  class="early-access-badge"
159
164
  >
160
- <i class="fa-solid fa-bolt" /> {{ $t("keys.campaigns.early_access") }}
165
+ <MGIcon size="1x" icon="clock" /> {{ $t("keys.campaigns.early_access") }}
161
166
  </div>
162
167
 
163
168
  <div
164
169
  v-if="card.streamer_type === 2 || card.streamer_type === 3"
165
170
  class="exclusive-badge level-badge"
166
171
  >
167
- <i class="fa-solid fa-user-check" />
172
+ <MGIcon size="1x" icon="verified" />
168
173
  {{
169
174
  card.streamer_type === 2
170
175
  ? $t("keys.campaigns.restriction.level_official")
@@ -176,7 +181,7 @@
176
181
  v-if="!card.user_can_access && card.available_at_for_user"
177
182
  class="early-access-lock-overlay"
178
183
  >
179
- <i class="fa-solid fa-lock" />
184
+ <MGIcon size="1x" icon="lock" />
180
185
  <span class="lock-label">{{ $t("keys.campaigns.early_access_lock_title") }}</span>
181
186
  <span class="lock-countdown">
182
187
  {{ $t("keys.campaigns.early_access_opens_in") }}
@@ -188,7 +193,7 @@
188
193
  v-else-if="!card.user_can_access && card.is_exclusive"
189
194
  class="exclusive-lock-overlay"
190
195
  >
191
- <i class="fa-solid fa-crown" />
196
+ <MGIcon size="1x" icon="reward" />
192
197
  <span class="lock-label">{{ $t("keys.campaigns.exclusive_lock_title") }}</span>
193
198
  <span class="lock-cta">{{ $t("keys.campaigns.exclusive_upgrade_cta") }}</span>
194
199
  </div>
@@ -208,6 +213,15 @@
208
213
  <div class="days" v-if="card.time > 0 && card.user_can_access">
209
214
  <span>{{ card.time }}</span> {{ $t("keys.campaigns.days") }}
210
215
  </div>
216
+ <!-- Labelled, next to the deadline: the two numbers a creator
217
+ actually decides on are "how long left" and "how many left". -->
218
+ <span
219
+ v-if="card.available_count > 0 && card.user_can_access && !card.isExpired && !card.isUpcoming"
220
+ class="card-keys-left"
221
+ >
222
+ <MGIcon size="1x" icon="giveaway" />
223
+ {{ $t("keys.campaigns.keys_left", { n: card.available_count }) }}
224
+ </span>
211
225
  <span>
212
226
  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="17" viewBox="0 0 16 17" fill="none">
213
227
  <path d="M13.3333 4.1112L12.8666 3.64453L11.9333 4.57786L10.9333 3.64453L8.33329 4.1112L2.66663 9.77786L7.19996 14.3112L12.8666 8.64453L13.3333 6.04453L12.4 5.1112L13.3333 4.1112ZM12.6 6.24453L12.2666 8.3112L7.19996 13.3779L3.59996 9.77786L8.66663 4.7112L10.7333 4.3112L11.4666 5.04453L11 5.5112L10.5333 5.04453L9.99996 5.57786L11.4 6.97786L11.8666 6.5112L11.4 6.04453L11.8666 5.57786L12.6 6.24453Z" fill="var(--key-accent, var(--key-accent, var(--primary, #D297FF)))" />
@@ -231,7 +245,7 @@
231
245
  </nuxt-link>
232
246
  <nuxt-link :to="upgradeRoute">
233
247
  <button class="btn secondary upgrade-btn">
234
- <i class="fa-solid fa-arrow-up" />
248
+ <MGIcon size="1x" icon="chevron-up" />
235
249
  {{ $t("keys.campaigns.upgrade_plan") }}
236
250
  </button>
237
251
  </nuxt-link>
@@ -473,6 +487,20 @@ function platformAbbr(name: string): string {
473
487
  return PLATFORM_ABBR[name.toLowerCase()] ?? name
474
488
  }
475
489
 
490
+ /**
491
+ * At or below this many keys left, the cover badge appears as a scarcity
492
+ * signal. Above it, the count is just a fact and lives in the info row.
493
+ */
494
+ const LOW_STOCK_THRESHOLD = 5
495
+
496
+ function showsLowStock(card: any): boolean {
497
+ return card.available_count > 0
498
+ && card.available_count <= LOW_STOCK_THRESHOLD
499
+ && card.user_can_access
500
+ && !card.isExpired
501
+ && !card.isUpcoming
502
+ }
503
+
476
504
  function cardTierLabel(tier: string): string {
477
505
  const labels: Record<string, string> = { free: "Free", pro: "PRO", business: "Business", official: "Official" }
478
506
  return $i18n.t("keys.campaigns.restriction.tier_locked", { tier: labels[tier] || tier })
@@ -874,6 +902,15 @@ onUnmounted(() => {
874
902
  opacity: 0.5;
875
903
  }
876
904
 
905
+ .card-keys-left {
906
+ display: inline-flex;
907
+ align-items: center;
908
+ gap: 4px;
909
+ white-space: nowrap;
910
+
911
+ i.icon { color: var(--key-accent, var(--primary, #D297FF)); }
912
+ }
913
+
877
914
  .keys-badge {
878
915
  position: absolute;
879
916
  top: 8px;
@@ -150,7 +150,7 @@
150
150
  <!-- Success modal -->
151
151
  <div v-if="showSuccessModal" class="success-modal-overlay" @click.self="handleSuccessClose">
152
152
  <div class="success-modal">
153
- <MGIcon size="2x" icon="check" style="color: #42FF00" />
153
+ <MGIcon size="2x" icon="verified" style="color: #42FF00" />
154
154
  <p>{{ $t("keys.materials.success_message") }}</p>
155
155
  <button class="btn button-submit" @click="handleSuccessClose">
156
156
  {{ $t("keys.materials.success_close") }}
@@ -248,7 +248,7 @@ function goBack() {
248
248
  <div v-else-if="error" class="state error">{{ error }}</div>
249
249
 
250
250
  <div v-else-if="success" class="state success">
251
- <MGIcon icon="check-circle" size="3x" />
251
+ <MGIcon icon="verified-filled" size="3x" />
252
252
  <p>{{ $t("keys.campaigns.request_sent") }}</p>
253
253
  <button class="btn" @click="goBack">
254
254
  {{ $t("keys.campaigns.back_to_campaign") }}
@@ -292,7 +292,7 @@ function goBack() {
292
292
  <div class="required-data">
293
293
  <div class="required-data-header" @click="toggleBox">
294
294
  <div class="required-data-header-left">
295
- <MGIcon size="1x" icon="check" :style="{ color: isBoxOpen ? 'var(--key-accent, var(--primary, #D297FF))' : 'var(--inactive)' }" />
295
+ <MGIcon size="1x" icon="verified" :style="{ color: isBoxOpen ? 'var(--key-accent, var(--primary, #D297FF))' : 'var(--inactive)' }" />
296
296
  <span>{{ $t("keys.campaigns.required_data") }}</span>
297
297
  </div>
298
298
  <button type="button" class="box-toggler" :aria-expanded="isBoxOpen">