@mundogamernetwork/shared-ui 1.11.1 → 1.11.3

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.
@@ -4,6 +4,7 @@ interface Video {
4
4
  title: string;
5
5
  url: string;
6
6
  type?: string;
7
+ thumbnail_url?: string;
7
8
  is_primary?: boolean;
8
9
  sort_order?: number;
9
10
  }
@@ -14,21 +15,27 @@ interface Props {
14
15
 
15
16
  const props = defineProps<Props>();
16
17
 
17
- const activeVideo = ref<Video | null>(null);
18
-
19
- onMounted(() => {
20
- const primary = props.videos.find(v => v.is_primary);
21
- activeVideo.value = primary || props.videos[0] || null;
22
- });
18
+ // Initialize synchronously so direct public-page loads also have a player
19
+ // during SSR, instead of waiting for client hydration.
20
+ const selectedVideoId = ref<number | null>(
21
+ (props.videos.find(v => v.is_primary) || props.videos[0])?.id ?? null,
22
+ );
23
+ const activeVideo = computed(() =>
24
+ props.videos.find(v => v.id === selectedVideoId.value) || props.videos[0] || null,
25
+ );
23
26
 
24
27
  function getEmbedUrl(url: string) {
25
- const ytMatch = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([\w-]+)/);
28
+ const ytMatch = url.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{11})/);
26
29
  if (ytMatch) return `https://www.youtube.com/embed/${ytMatch[1]}`;
27
30
  const vimeoMatch = url.match(/vimeo\.com\/(\d+)/);
28
31
  if (vimeoMatch) return `https://player.vimeo.com/video/${vimeoMatch[1]}`;
29
32
  return url;
30
33
  }
31
34
 
35
+ function isDirectVideo(url: string) {
36
+ return /\.(mp4|webm|ogg)(?:[?#].*)?$/i.test(url);
37
+ }
38
+
32
39
  function formatType(type?: string) {
33
40
  if (!type) return '';
34
41
  return type.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
@@ -40,7 +47,9 @@ function formatType(type?: string) {
40
47
  <h3 class="section-title">{{ $t('press_kit.videos') }}</h3>
41
48
 
42
49
  <div v-if="activeVideo" class="video-embed">
50
+ <video v-if="isDirectVideo(activeVideo.url)" :src="activeVideo.url" :poster="activeVideo.thumbnail_url || undefined" controls preload="metadata" />
43
51
  <iframe
52
+ v-else
44
53
  :src="getEmbedUrl(activeVideo.url)"
45
54
  frameborder="0"
46
55
  allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
@@ -53,7 +62,7 @@ function formatType(type?: string) {
53
62
  v-for="video in videos"
54
63
  :key="video.id"
55
64
  :class="['video-item', { active: activeVideo?.id === video.id }]"
56
- @click="activeVideo = video"
65
+ @click="selectedVideoId = video.id"
57
66
  >
58
67
  <MGIcon icon="videos" />
59
68
  <div class="video-meta">
@@ -86,6 +95,12 @@ function formatType(type?: string) {
86
95
  width: 100%;
87
96
  height: 100%;
88
97
  }
98
+
99
+ video {
100
+ width: 100%;
101
+ height: 100%;
102
+ object-fit: contain;
103
+ }
89
104
  }
90
105
 
91
106
  .video-list {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.11.1",
3
+ "version": "1.11.3",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -313,8 +313,8 @@
313
313
  </div>
314
314
 
315
315
  <!-- Step 2: My requests -->
316
- <div class="step-inside" v-if="currentStep === 2">
317
- <div class="header">
316
+ <div class="step-inside requests-step" v-if="currentStep === 2">
317
+ <div class="header requests-toolbar">
318
318
  <div class="header-left">
319
319
  <button
320
320
  class="btn tertiary"
@@ -334,12 +334,16 @@
334
334
  </button>
335
335
  </div>
336
336
  <div class="bar">
337
- <input
338
- type="text"
339
- v-model="searchTerm"
340
- :placeholder="$t('keys.campaigns.search_placeholder')"
341
- />
342
- <button type="button"><MGIcon icon="search" /></button>
337
+ <div class="search-box">
338
+ <input
339
+ type="text"
340
+ v-model="searchTerm"
341
+ :placeholder="$t('keys.campaigns.search_placeholder')"
342
+ />
343
+ <button type="button" :aria-label="$t('keys.campaigns.search_placeholder')">
344
+ <MGIcon icon="search" />
345
+ </button>
346
+ </div>
343
347
  </div>
344
348
  </div>
345
349
 
@@ -369,31 +373,45 @@
369
373
  <p v-if="calendarError" class="calendar-export__error">{{ calendarError }}</p>
370
374
  </div>
371
375
 
372
- <div class="cards mt-5">
376
+ <div class="cards request-cards">
373
377
  <template v-if="loadingRequests">
374
- <div class="card skeleton" v-for="n in 3" :key="n"></div>
378
+ <div class="card request-card skeleton" v-for="n in 3" :key="n"></div>
375
379
  </template>
376
380
  <template v-else>
377
- <div class="card" v-for="card in filteredCards" :key="card.id">
381
+ <nuxt-link
382
+ class="card request-card"
383
+ v-for="card in filteredCards"
384
+ :key="card.id"
385
+ :to="`/${locale}/key-campaigns/${card.key.slug}`"
386
+ >
378
387
  <div class="card-header">
379
- <nuxt-link :to="`/${locale}/key-campaigns/${card.key.slug}`">
380
- <div class="img-content">
381
- <img :src="card.key.cover_src" :alt="card.key.name" />
388
+ <div class="img-content">
389
+ <img
390
+ :src="card.key.cover_src || '/imgs/no-cover-img.jpg'"
391
+ :alt="card.key.name"
392
+ @error="(e) => { (e.target as HTMLImageElement).src = '/imgs/no-cover-img.jpg' }"
393
+ />
394
+ <div class="request-status" :style="{ '--request-status-color': card.color }">
395
+ <span class="request-status__dot"></span>
396
+ {{ $t(card.status) }}
382
397
  </div>
383
- </nuxt-link>
398
+ </div>
384
399
  </div>
385
- <div class="text">
386
- {{ card.key.name }}
387
- <div class="status">
388
- <span :style="{ color: card.color }">◉</span>
389
- {{ $t(card.status) }}
400
+ <div class="text request-card__body">
401
+ <div class="card-name">{{ card.key.game?.name || card.key.name }}</div>
402
+ <div v-if="card.key.game?.name && card.key.name !== card.key.game.name" class="request-card__campaign">
403
+ {{ card.key.name }}
404
+ </div>
405
+ <div class="request-card__footer">
406
+ <span v-if="card.key.type?.name" class="request-card__type">{{ card.key.type.name }}</span>
407
+ <MGIcon icon="chevron-right" />
390
408
  </div>
391
409
  </div>
392
- </div>
410
+ </nuxt-link>
393
411
  </template>
394
412
  </div>
395
413
 
396
- <div v-if="!loadingRequests && filteredCards.length === 0" class="d-flex flex-column align-items-center">
414
+ <div v-if="!loadingRequests && filteredCards.length === 0" class="requests-empty">
397
415
  <div class="no-content"><MGIcon icon="eye-slash" /></div>
398
416
  <h4>{{ $t("keys.campaigns.no_results") }}</h4>
399
417
  </div>
@@ -920,12 +938,31 @@ onUnmounted(() => {
920
938
  display: flex;
921
939
  align-items: center;
922
940
  flex-wrap: wrap;
923
- gap: 8px;
924
- margin-bottom: 16px;
941
+ gap: 10px;
942
+ margin: 0 0 28px;
943
+ padding: 16px 18px;
944
+ border: 1px solid var(--key-border, rgba(255, 255, 255, 0.1));
945
+ background: var(--body-bg-card, rgba(255, 255, 255, 0.035));
925
946
 
926
947
  &__label {
948
+ margin-right: auto;
927
949
  font-size: 13px;
928
- opacity: 0.75;
950
+ font-weight: 500;
951
+ color: var(--secondary-info-fg);
952
+ }
953
+
954
+ .btn {
955
+ display: inline-flex;
956
+ align-items: center;
957
+ gap: 7px;
958
+ min-height: 38px;
959
+ border: 1px solid var(--key-border, rgba(255, 255, 255, 0.14));
960
+ background: var(--button-secondary-default-bg, #1e1e1e);
961
+
962
+ &:hover:not(:disabled) {
963
+ border-color: var(--key-accent, var(--primary, #D297FF));
964
+ color: var(--key-accent, var(--primary, #D297FF));
965
+ }
929
966
  }
930
967
 
931
968
  &__error {
@@ -937,6 +974,145 @@ onUnmounted(() => {
937
974
  }
938
975
  }
939
976
 
977
+ .requests-step {
978
+ .requests-toolbar {
979
+ margin: 18px 0;
980
+ padding: 14px !important;
981
+ border: 1px solid var(--key-border, rgba(255, 255, 255, 0.08));
982
+ background: var(--body-bg-card, rgba(255, 255, 255, 0.025));
983
+
984
+ .header-left {
985
+ flex-wrap: wrap;
986
+ gap: 8px !important;
987
+
988
+ button {
989
+ height: 38px !important;
990
+ padding-inline: 16px;
991
+ border: 1px solid transparent;
992
+ }
993
+ }
994
+
995
+ .search-box { width: min(320px, 100%); }
996
+ }
997
+
998
+ .request-cards {
999
+ display: grid !important;
1000
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1001
+ gap: 20px !important;
1002
+ margin: 0 0 48px !important;
1003
+ }
1004
+
1005
+ .request-card {
1006
+ min-width: 0 !important;
1007
+ max-width: none !important;
1008
+ width: 100%;
1009
+ color: inherit;
1010
+ text-decoration: none;
1011
+ border: 1px solid var(--key-border, rgba(255, 255, 255, 0.08)) !important;
1012
+ overflow: hidden;
1013
+
1014
+ &:hover {
1015
+ border-color: var(--key-accent, var(--primary, #D297FF)) !important;
1016
+ transform: translateY(-4px);
1017
+ box-shadow: 0 12px 30px var(--key-accent-shadow, rgba(107, 21, 172, 0.18));
1018
+ text-decoration: none;
1019
+ }
1020
+
1021
+ .card-header .img-content {
1022
+ min-height: 168px !important;
1023
+ max-height: 168px !important;
1024
+
1025
+ img {
1026
+ width: 100% !important;
1027
+ height: 168px !important;
1028
+ object-fit: cover;
1029
+ }
1030
+
1031
+ &::after {
1032
+ content: "";
1033
+ position: absolute;
1034
+ inset: auto 0 0;
1035
+ height: 55%;
1036
+ background: linear-gradient(transparent, rgba(0, 0, 0, 0.72));
1037
+ pointer-events: none;
1038
+ }
1039
+ }
1040
+
1041
+ &__body {
1042
+ min-height: 132px !important;
1043
+ gap: 7px !important;
1044
+ }
1045
+
1046
+ &__campaign {
1047
+ font-size: 12px;
1048
+ font-weight: 400;
1049
+ color: var(--secondary-info-fg);
1050
+ overflow: hidden;
1051
+ text-overflow: ellipsis;
1052
+ white-space: nowrap;
1053
+ }
1054
+
1055
+ &__footer {
1056
+ display: flex;
1057
+ align-items: center;
1058
+ justify-content: space-between;
1059
+ gap: 10px;
1060
+ margin-top: auto;
1061
+ padding-top: 12px;
1062
+ border-top: 1px solid var(--key-border, rgba(255, 255, 255, 0.08));
1063
+ color: var(--secondary-info-fg);
1064
+
1065
+ i { color: var(--key-accent, var(--primary, #D297FF)); }
1066
+ }
1067
+
1068
+ &__type {
1069
+ font-size: 11px;
1070
+ font-weight: 600;
1071
+ text-transform: uppercase;
1072
+ letter-spacing: 0.04em;
1073
+ }
1074
+ }
1075
+
1076
+ .request-status {
1077
+ position: absolute;
1078
+ z-index: 2;
1079
+ left: 12px;
1080
+ bottom: 12px;
1081
+ display: inline-flex;
1082
+ align-items: center;
1083
+ gap: 7px;
1084
+ padding: 6px 9px;
1085
+ background: rgba(8, 8, 10, 0.88);
1086
+ border: 1px solid color-mix(in srgb, var(--request-status-color, #D297FF) 55%, transparent);
1087
+ color: #fff;
1088
+ font-size: 11px;
1089
+ font-weight: 600;
1090
+ text-transform: uppercase;
1091
+ letter-spacing: 0.03em;
1092
+
1093
+ &__dot {
1094
+ width: 7px;
1095
+ height: 7px;
1096
+ border-radius: 50%;
1097
+ background: var(--request-status-color, #D297FF);
1098
+ box-shadow: 0 0 8px var(--request-status-color, #D297FF);
1099
+ }
1100
+ }
1101
+
1102
+ .requests-empty {
1103
+ display: flex;
1104
+ min-height: 280px;
1105
+ flex-direction: column;
1106
+ align-items: center;
1107
+ justify-content: center;
1108
+ border: 1px dashed var(--key-border, rgba(255, 255, 255, 0.14));
1109
+ background: var(--body-bg-card, rgba(255, 255, 255, 0.02));
1110
+
1111
+ .no-content { margin-top: 0 !important; }
1112
+ h4 { margin: 0; font-size: 15px; }
1113
+ }
1114
+ }
1115
+
940
1116
  .spinner {
941
1117
  display: inline-block;
942
1118
  width: 20px;
@@ -1475,6 +1651,25 @@ onUnmounted(() => {
1475
1651
  }
1476
1652
 
1477
1653
  @media (max-width: 767px) {
1654
+ .requests-step {
1655
+ .requests-toolbar {
1656
+ align-items: stretch !important;
1657
+ padding: 12px !important;
1658
+
1659
+ .header-left { width: 100%; }
1660
+ .bar, .search-box { width: 100% !important; }
1661
+ }
1662
+
1663
+ .request-cards { grid-template-columns: 1fr; }
1664
+ }
1665
+
1666
+ .calendar-export {
1667
+ align-items: stretch;
1668
+
1669
+ &__label { flex-basis: 100%; margin-bottom: 4px; }
1670
+ .btn { flex: 1; justify-content: center; }
1671
+ }
1672
+
1478
1673
  .stepper-content .step-inside {
1479
1674
  .header {
1480
1675
  flex-direction: column;
@@ -1492,6 +1687,10 @@ onUnmounted(() => {
1492
1687
  .slider-container .slide { height: auto; }
1493
1688
  }
1494
1689
  }
1690
+
1691
+ @media (min-width: 768px) and (max-width: 1023px) {
1692
+ .requests-step .request-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
1693
+ }
1495
1694
  }
1496
1695
  </style>
1497
1696