@mundogamernetwork/shared-ui 1.8.8 → 1.8.13

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.
@@ -725,18 +725,15 @@ body:has(.kit-press) {
725
725
  margin: 0;
726
726
  }
727
727
  .kit-footer__cta {
728
- display: inline-block;
729
- padding: 8px 20px;
730
- border: 1px solid var(--kit-accent, #FDB215);
731
728
  color: var(--kit-accent, #FDB215);
732
729
  font-size: 0.8rem;
733
730
  font-weight: 600;
734
731
  text-decoration: none;
735
- transition: background 0.15s, color 0.15s;
732
+ border-bottom: 1px solid transparent;
733
+ transition: border-color 0.15s;
736
734
  }
737
735
  .kit-footer__cta:hover {
738
- background: var(--kit-accent, #FDB215);
739
- color: #000;
736
+ border-bottom-color: var(--kit-accent, #FDB215);
740
737
  }
741
738
 
742
739
  /* ── Scroll offset for sticky anchor nav ────────────────────────────*/
@@ -0,0 +1,61 @@
1
+ <script setup lang="ts">
2
+ // Inline SVG per platform instead of Font Awesome <i class="fa-..."> classes —
3
+ // shared-ui can't control whether a consuming app wires up FA as a classic
4
+ // webfont/CSS kit or as SVG Vue components (agency-frontend uses the latter,
5
+ // via @fortawesome/vue-fontawesome, and only registers the solid icon set —
6
+ // no brands set at all), so raw `<i class="fa-brands fa-tiktok">` silently
7
+ // renders nothing there. Built from simple SVG primitives (circle/rect/line),
8
+ // not memorized brand path data, so correctness doesn't depend on visually
9
+ // checking curve coordinates.
10
+ defineProps<{
11
+ platform: string
12
+ }>()
13
+ </script>
14
+
15
+ <template>
16
+ <svg v-if="platform === 'x'" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
17
+ <line x1="4" y1="4" x2="20" y2="20" /><line x1="20" y1="4" x2="4" y2="20" />
18
+ </svg>
19
+ <svg v-else-if="platform === 'youtube'" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5">
20
+ <rect x="2" y="5" width="20" height="14" rx="4" />
21
+ <polygon points="10,9 10,15 15,12" fill="currentColor" stroke="none" />
22
+ </svg>
23
+ <svg v-else-if="platform === 'instagram'" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5">
24
+ <rect x="3" y="3" width="18" height="18" rx="5" />
25
+ <circle cx="12" cy="12" r="4" />
26
+ <circle cx="17.5" cy="6.5" r="1" fill="currentColor" stroke="none" />
27
+ </svg>
28
+ <svg v-else-if="platform === 'facebook'" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5">
29
+ <circle cx="12" cy="12" r="9" />
30
+ <text x="12" y="16.5" text-anchor="middle" font-size="11" font-weight="700" fill="currentColor" stroke="none">f</text>
31
+ </svg>
32
+ <svg v-else-if="platform === 'linkedin'" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5">
33
+ <rect x="3" y="3" width="18" height="18" rx="3" />
34
+ <text x="12" y="16" text-anchor="middle" font-size="9" font-weight="700" fill="currentColor" stroke="none">in</text>
35
+ </svg>
36
+ <svg v-else-if="platform === 'discord'" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5">
37
+ <rect x="3" y="6" width="18" height="12" rx="6" />
38
+ <circle cx="9" cy="12" r="1.3" fill="currentColor" stroke="none" />
39
+ <circle cx="15" cy="12" r="1.3" fill="currentColor" stroke="none" />
40
+ </svg>
41
+ <svg v-else-if="platform === 'twitch'" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5">
42
+ <rect x="3" y="4" width="18" height="13" rx="1" />
43
+ <line x1="9" y1="7" x2="9" y2="13" /><line x1="15" y1="7" x2="15" y2="13" />
44
+ </svg>
45
+ <svg v-else-if="platform === 'tiktok'" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5">
46
+ <circle cx="9" cy="17" r="3" />
47
+ <line x1="12" y1="17" x2="12" y2="5" />
48
+ <path d="M12 5c0 2.5 2 4.5 4.5 4.5" />
49
+ </svg>
50
+ <svg v-else-if="platform === 'telegram'" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round">
51
+ <path d="M3 12.5 20 4l-3.5 16-5-4-3 3-1-5.5z" />
52
+ </svg>
53
+ <span v-else class="platform-icon-fallback">{{ platform.slice(0, 2).toUpperCase() }}</span>
54
+ </template>
55
+
56
+ <style scoped>
57
+ .platform-icon-fallback {
58
+ font-size: 11px;
59
+ font-weight: 700;
60
+ }
61
+ </style>
@@ -22,7 +22,7 @@ const props = defineProps<{
22
22
  isUpcoming?: boolean;
23
23
  streamer_type?: number | null;
24
24
  type?: string | null;
25
- platforms?: Array<{ id: number; name: string; slug?: string | null } | string>;
25
+ platforms?: Array<{ id: number; name: string; slug?: string | null; alternative_name?: string | null } | string>;
26
26
  };
27
27
  publicMode?: boolean;
28
28
  baseRoute?: string;
@@ -65,9 +65,12 @@ const PLATFORM_ABBR: Record<string, string> = {
65
65
  "linux": "Linux", "ios": "iOS", "android": "Android", "gog": "GOG",
66
66
  };
67
67
 
68
- function platformAbbr(p: { name: string } | string): string {
69
- const name = typeof p === "string" ? p : p.name;
70
- return PLATFORM_ABBR[name.toLowerCase()] ?? name;
68
+ // The API already resolves an abbreviated name per platform (community.platforms
69
+ // .alternative_name, e.g. "Playstation 4" "PS4") prefer that over the static
70
+ // map below, which only exists as a fallback for older responses that omit it.
71
+ function platformAbbr(p: { name: string; alternative_name?: string | null } | string): string {
72
+ if (typeof p === "string") return PLATFORM_ABBR[p.toLowerCase()] ?? p;
73
+ return p.alternative_name || PLATFORM_ABBR[p.name.toLowerCase()] || p.name;
71
74
  }
72
75
 
73
76
  function cardTierLabel(tier: string | null | undefined): string {
@@ -1,7 +1,6 @@
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"
5
4
 
6
5
  const props = defineProps<{
7
6
  excludeId?: number | null
@@ -11,10 +10,16 @@ const props = defineProps<{
11
10
  }>()
12
11
 
13
12
  const mgPlatform = props.mgPlatform || (inject<string>("mgPlatform", "MGTV"))
13
+ const locale = useNuxtApp().$i18n.locale
14
14
 
15
15
  const items = ref<any[]>([])
16
16
  const loading = ref(true)
17
17
 
18
+ const detailRoute = (item: any) =>
19
+ props.publicMode
20
+ ? `/${locale.value}/${props.baseRoute ?? 'key-campaigns'}/${item.slug}`
21
+ : `/${locale.value}/dashboard/${props.baseRoute ?? 'key-campaigns'}/${item.slug}`
22
+
18
23
  onMounted(async () => {
19
24
  try {
20
25
  const res = await fetchCampaigns(
@@ -49,13 +54,31 @@ onMounted(async () => {
49
54
  </div>
50
55
 
51
56
  <div v-else class="kc-carousel__track">
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>
57
+ <NuxtLink
58
+ v-for="item in items"
59
+ :key="item.id"
60
+ :to="detailRoute(item)"
61
+ class="kc-carousel__card"
62
+ >
63
+ <div class="kc-carousel__thumb">
64
+ <img
65
+ v-if="item.cover_src || item.cover"
66
+ :src="item.cover_src || item.cover"
67
+ :alt="item.name"
68
+ />
69
+ <div v-else class="kc-carousel__thumb-placeholder">
70
+ <i class="mg-icon mg-icon-game-key" />
71
+ </div>
72
+ <span v-if="!item.is_open" class="kc-carousel__closed">{{ $t("keys.campaigns.closed") }}</span>
73
+ </div>
74
+ <div class="kc-carousel__info">
75
+ <div class="kc-carousel__name">{{ item.name }}</div>
76
+ <div class="kc-carousel__avail">
77
+ {{ item.available_count ?? 0 }}/{{ item.total_count ?? item.quantity ?? "?" }}
78
+ {{ $t("keys.campaigns.available") }}
79
+ </div>
80
+ </div>
81
+ </NuxtLink>
59
82
  </div>
60
83
  </div>
61
84
  </template>
@@ -90,14 +113,81 @@ onMounted(async () => {
90
113
  }
91
114
 
92
115
  &__skeleton {
93
- flex: 0 0 200px;
94
- height: 280px;
116
+ flex: 0 0 180px;
117
+ height: 140px;
95
118
  background: var(--bg-app-badge);
96
119
  animation: pulse 1.4s ease-in-out infinite;
97
120
  }
98
121
 
99
122
  &__card {
100
- flex: 0 0 200px;
123
+ flex: 0 0 180px;
124
+ display: flex;
125
+ flex-direction: column;
126
+ text-decoration: none;
127
+ color: inherit;
128
+ border: 1px solid var(--button-secondary-default-bg);
129
+ transition: border-color 0.15s;
130
+
131
+ &:hover { border-color: var(--key-accent, var(--primary, #D297FF)); }
132
+ }
133
+
134
+ &__thumb {
135
+ position: relative;
136
+ width: 100%;
137
+ height: 101px;
138
+ overflow: hidden;
139
+ background: var(--bg-app-badge);
140
+
141
+ img {
142
+ width: 100%;
143
+ height: 100%;
144
+ object-fit: cover;
145
+ }
146
+ }
147
+
148
+ &__thumb-placeholder {
149
+ width: 100%;
150
+ height: 100%;
151
+ display: flex;
152
+ align-items: center;
153
+ justify-content: center;
154
+ color: var(--secondary-info-fg);
155
+ font-size: 24px;
156
+ }
157
+
158
+ &__closed {
159
+ position: absolute;
160
+ inset: 0;
161
+ display: flex;
162
+ align-items: center;
163
+ justify-content: center;
164
+ background: var(--overlay-dark, rgba(0, 0, 0, 0.55));
165
+ font-size: 11px;
166
+ font-weight: 700;
167
+ color: var(--card-article-title, #fff);
168
+ letter-spacing: 0.5px;
169
+ text-transform: uppercase;
170
+ }
171
+
172
+ &__info {
173
+ padding: 8px 10px;
174
+ display: flex;
175
+ flex-direction: column;
176
+ gap: 4px;
177
+ }
178
+
179
+ &__name {
180
+ font-size: 12px;
181
+ font-weight: 600;
182
+ color: var(--card-article-title);
183
+ overflow: hidden;
184
+ text-overflow: ellipsis;
185
+ white-space: nowrap;
186
+ }
187
+
188
+ &__avail {
189
+ font-size: 11px;
190
+ color: var(--secondary-info-fg);
101
191
  }
102
192
  }
103
193
 
package/nuxt.config.ts CHANGED
@@ -65,6 +65,10 @@ export default defineNuxtConfig({
65
65
  // this reaches every consuming app automatically — matches the storesDirs
66
66
  // pattern below. @vue-leaflet/vue-leaflet ships untranspiled ESM that
67
67
  // breaks Nuxt's build without this, needed by MediaKit/MapCountries.vue.
68
+ // Safe now that leaflet/@vue-leaflet are real dependencies (not
69
+ // peerDependencies) of this package — every consumer gets them
70
+ // transitively via node_modules hoisting, so the module this transpile
71
+ // entry points at always actually exists to resolve.
68
72
  build: {
69
73
  transpile: ['@vue-leaflet/vue-leaflet'],
70
74
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.8.8",
3
+ "version": "1.8.13",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -30,25 +30,25 @@
30
30
  "peerDependencies": {
31
31
  "@pinia-plugin-persistedstate/nuxt": ">=1.0.0",
32
32
  "@pinia/nuxt": ">=0.5.0",
33
- "@vue-leaflet/vue-leaflet": ">=0.10.0",
34
33
  "axios": ">=1.0.0",
35
34
  "hls.js": ">=1.5.0",
36
35
  "laravel-echo": ">=1.15.0",
37
- "leaflet": ">=1.9.0",
38
36
  "nuxt": ">=3.13.0",
39
37
  "pinia": ">=2.1.0",
40
38
  "pusher-js": ">=8.0.0",
41
39
  "vue": ">=3.5.0"
42
40
  },
41
+ "dependencies": {
42
+ "@vue-leaflet/vue-leaflet": "^0.10.1",
43
+ "leaflet": "^1.9.4"
44
+ },
43
45
  "devDependencies": {
44
46
  "@pinia-plugin-persistedstate/nuxt": "^1.2.1",
45
47
  "@pinia/nuxt": "^0.5.5",
46
48
  "@types/leaflet": "^1.9.18",
47
- "@vue-leaflet/vue-leaflet": "^0.10.1",
48
49
  "axios": "^1.18.1",
49
50
  "hls.js": "^1.6.16",
50
51
  "laravel-echo": "^2.3.7",
51
- "leaflet": "^1.9.4",
52
52
  "nuxt": "^3.15.4",
53
53
  "pinia": "^2.3.1",
54
54
  "pusher-js": "^8.5.0",
@@ -1178,7 +1178,12 @@ onUnmounted(() => {
1178
1178
 
1179
1179
  .cards {
1180
1180
  display: flex;
1181
- align-items: flex-start;
1181
+ // Cards in the same row have wildly different content amounts
1182
+ // (a badge here, an extra wrapped platform tag there, "days
1183
+ // remaining" not always present) — stretch instead of
1184
+ // flex-start so every card in a row shares the row's height,
1185
+ // with .buttons pinned to the bottom (see .text/.buttons below).
1186
+ align-items: stretch;
1182
1187
  gap: 24px;
1183
1188
  height: 100%;
1184
1189
  flex-flow: wrap;
@@ -1235,6 +1240,7 @@ onUnmounted(() => {
1235
1240
  }
1236
1241
 
1237
1242
  .text {
1243
+ flex: 1;
1238
1244
  min-height: 170px;
1239
1245
 
1240
1246
  .card-name {
@@ -1307,6 +1313,10 @@ onUnmounted(() => {
1307
1313
  justify-content: space-between;
1308
1314
  align-items: center;
1309
1315
  width: 100%;
1316
+ // Pins the buttons row to the bottom of .text (now
1317
+ // flex:1) regardless of how much content sits above
1318
+ // it — every card's actions line up on the same row.
1319
+ margin-top: auto;
1310
1320
 
1311
1321
  .btn-share {
1312
1322
  background: transparent;
@@ -4,6 +4,7 @@ import { sendMediaKitProposal } from '../../services/mediaKitProposalService';
4
4
  import MediaKitWallBlock from '../../components/indie-wall/MediaKitWallBlock.vue';
5
5
  import MgMediaKitStore from '../../components/MediaKit/MgMediaKitStore.vue';
6
6
  import MapCountries from '../../components/MediaKit/MapCountries.vue';
7
+ import PlatformIcon from '../../components/PlatformIcon.vue';
7
8
 
8
9
  const route = useRoute();
9
10
  const slug = computed(() => route.params.slug as string);
@@ -121,14 +122,6 @@ const PLATFORM_LABEL: Record<string, string> = {
121
122
  linkedin: 'LinkedIn', bluesky: 'Bluesky',
122
123
  };
123
124
 
124
- // Font Awesome brand icon per platform — no entry means no official FA glyph
125
- // exists yet (e.g. Kick), the template falls back to the 2-letter label.
126
- const PLATFORM_ICON: Record<string, string> = {
127
- instagram: 'fa-instagram', tiktok: 'fa-tiktok', youtube: 'fa-youtube', twitch: 'fa-twitch',
128
- x: 'fa-x-twitter', facebook: 'fa-facebook', discord: 'fa-discord', telegram: 'fa-telegram',
129
- linkedin: 'fa-linkedin', bluesky: 'fa-bluesky',
130
- };
131
-
132
125
  const socials = computed(() => {
133
126
  const s = streamer.value?.social_links || {};
134
127
  return Object.entries(s).filter(([, v]) => v).map(([k, v]) => ({ platform: k, url: v as string }));
@@ -358,8 +351,7 @@ function onDownload() {
358
351
 
359
352
  <div v-if="socials.length" class="mk-socials no-print">
360
353
  <a v-for="s in socials" :key="s.platform" class="mk-soc" :href="s.url" target="_blank" rel="noopener" :title="PLATFORM_LABEL[s.platform] || s.platform" @click="track('click')">
361
- <i v-if="PLATFORM_ICON[s.platform]" class="fa-brands" :class="PLATFORM_ICON[s.platform]" />
362
- <span v-else class="mk-soc-fallback">{{ (PLATFORM_LABEL[s.platform] || s.platform).slice(0, 2).toUpperCase() }}</span>
354
+ <PlatformIcon :platform="s.platform" />
363
355
  </a>
364
356
  </div>
365
357
  </aside>
@@ -396,7 +388,7 @@ function onDownload() {
396
388
  <a v-for="p in platforms" :key="p.platform" class="mk-platform" :href="p.url || undefined" target="_blank" rel="noopener" @click="track('click')">
397
389
  <div class="pl-head">
398
390
  <span class="pl-name-group">
399
- <i v-if="PLATFORM_ICON[p.platform]" class="fa-brands pl-icon" :class="PLATFORM_ICON[p.platform]" />
391
+ <PlatformIcon class="pl-icon" :platform="p.platform" />
400
392
  <span class="pl-name">{{ PLATFORM_LABEL[p.platform] || p.platform }}</span>
401
393
  </span>
402
394
  <span v-if="p.verified" class="pl-verified" :title="$t('kit.media.verified')">✓</span>
@@ -646,10 +638,8 @@ function onDownload() {
646
638
  <MediaKitWallBlock :user-id="userId" @wall-status="hasMural = $event" />
647
639
  </section>
648
640
 
649
- <footer class="mk-footer">
650
- <p v-if="!isPremium" class="kit-footer__text">{{ $t('kit.press.made_with') }} <strong>Mundo Gamer Agency</strong></p>
651
- <a v-if="!isPremium" :href="`${agencyBase}/dashboard/media-kit`" target="_blank" rel="noopener" class="kit-footer__cta">{{ $t('kit.media.create_yours') }}</a>
652
- <p class="mk-footer-meta">{{ $t('kit.media.generated') }} {{ kit.generated_at?.slice(0, 10) }}</p>
641
+ <footer v-if="!isPremium" class="mk-footer">
642
+ <p class="kit-footer__text">{{ $t('kit.press.made_with') }} <strong>Mundo Gamer Agency</strong> · <a :href="`${agencyBase}/dashboard/media-kit`" target="_blank" rel="noopener" class="kit-footer__cta">{{ $t('kit.media.create_yours') }}</a></p>
653
643
  </footer>
654
644
  </div>
655
645
  </template>
@@ -821,7 +811,6 @@ function onDownload() {
821
811
 
822
812
  &:hover { border-color: var(--kit-accent); color: var(--kit-accent); }
823
813
  }
824
- .mk-soc-fallback { font-size: 11px; font-weight: 700; }
825
814
 
826
815
  /* Buttons (shared) */
827
816
  .kit-btn {
@@ -1095,11 +1084,6 @@ function onDownload() {
1095
1084
  text-align: center;
1096
1085
  strong { color: var(--kit-accent); font-weight: 700; }
1097
1086
  }
1098
- .mk-footer-meta {
1099
- color: var(--kit-muted);
1100
- font-size: 13px;
1101
- margin: 0;
1102
- }
1103
1087
 
1104
1088
  /* Responsive */
1105
1089
  @media (max-width: 840px) {
@@ -619,8 +619,8 @@ useHead(() => ({
619
619
  </section>
620
620
 
621
621
  <footer class="kit-footer">
622
- <p v-if="!kit.no_branding" class="kit-footer__text">{{ $t('kit.press.made_with') }} <strong>Mundo Gamer Agency</strong></p>
623
- <a :href="kit.create_kit_url || `${agencyBase}/presskit`" target="_blank" rel="noopener" class="kit-footer__cta">{{ $t('kit.press.create_yours') }}</a>
622
+ <p v-if="!kit.no_branding" class="kit-footer__text">{{ $t('kit.press.made_with') }} <strong>Mundo Gamer Agency</strong> · <a :href="kit.create_kit_url || `${agencyBase}/presskit`" target="_blank" rel="noopener" class="kit-footer__cta">{{ $t('kit.press.create_yours') }}</a></p>
623
+ <a v-else :href="kit.create_kit_url || `${agencyBase}/presskit`" target="_blank" rel="noopener" class="kit-footer__cta">{{ $t('kit.press.create_yours') }}</a>
624
624
  </footer>
625
625
  </template>
626
626
  </div>
@@ -157,7 +157,7 @@ export interface Campaign {
157
157
  status?: number | boolean
158
158
  quantity?: number
159
159
  request_user_status?: number
160
- platforms?: Array<{ id: number; name: string; slug: string }>
160
+ platforms?: Array<{ id: number; name: string; slug: string; alternative_name?: string | null }>
161
161
  regions?: Array<{ id: number; name: string; slug: string }>
162
162
  requests?: Array<{ id: number; key_item_id?: number; admin_notes?: string }>
163
163
  company?: { name: string; email?: string; twitter?: string; thumbnail_url?: string }
@@ -187,6 +187,7 @@ export interface Platform {
187
187
  id: number
188
188
  name: string
189
189
  slug: string
190
+ alternative_name?: string | null
190
191
  }
191
192
 
192
193
  export interface CampaignsResponse {
@@ -332,7 +333,7 @@ export const fetchCampaignBySlug = async (slug: string) => {
332
333
  request_id: k.requests?.[0]?.id,
333
334
  request_item_id: k.requests?.[0]?.key_item_id,
334
335
  request_admin_notes: k.requests?.[0]?.admin_notes ?? null,
335
- platforms: (k.platforms || []).map((p: any) => ({ id: p.id, name: p.name, slug: p.slug })),
336
+ platforms: (k.platforms || []).map((p: any) => ({ id: p.id, name: p.name, slug: p.slug, alternative_name: p.alternative_name ?? null })),
336
337
  regions: (k.regions || []).map((r: any) => ({ id: r.id, name: r.name, slug: r.slug })),
337
338
  }
338
339
  }