@mundogamernetwork/shared-ui 1.0.3 → 1.1.1

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.
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <MgErrorBase code="401">
3
+ <template #text>
4
+ <p class="error-section__title">{{ $t('errors.401.title') }}</p><br/>
5
+ <p class="error-section__description">{{ $t('errors.401.description.text_1') }}</p>
6
+ <p class="error-section__description">{{ $t('errors.401.description.text_2') }}</p>
7
+ </template>
8
+ </MgErrorBase>
9
+ </template>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <MgErrorBase code="403">
3
+ <template #text>
4
+ <p class="error-section__title">{{ $t('errors.403.title') }}</p><br/>
5
+ <p class="error-section__description">{{ $t('errors.403.description.text_1') }}</p>
6
+ <p class="error-section__description">{{ $t('errors.403.description.text_2') }}</p>
7
+ </template>
8
+ </MgErrorBase>
9
+ </template>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <MgErrorBase code="404">
3
+ <template #text>
4
+ <p class="error-section__title">{{ $t('errors.404.title') }}</p><br/>
5
+ <p class="error-section__description">{{ $t('errors.404.description') }}</p>
6
+ <ul class="error-section__description">
7
+ <li>{{ $t('errors.404.solutions.a') }}</li>
8
+ <li>{{ $t('errors.404.solutions.b') }}</li>
9
+ <li>{{ $t('errors.404.solutions.c') }}</li>
10
+ </ul>
11
+ </template>
12
+ </MgErrorBase>
13
+ </template>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <MgErrorBase code="500">
3
+ <template #text>
4
+ <h1 class="error-section__title">{{ $t('errors.500.title') }}</h1><br/>
5
+ <p class="error-section__description">{{ $t('errors.500.description.text_1') }}</p>
6
+ <p class="error-section__description">{{ $t('errors.500.description.text_2') }}</p>
7
+ </template>
8
+ </MgErrorBase>
9
+ </template>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <MgErrorBase code="503">
3
+ <template #text>
4
+ <h1 class="error-section__title">{{ $t('errors.503.title') }}</h1><br/>
5
+ <p class="error-section__description">{{ $t('errors.503.description.text_1') }}</p>
6
+ <p class="error-section__description">{{ $t('errors.503.description.text_2') }}</p>
7
+ </template>
8
+ </MgErrorBase>
9
+ </template>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <MgErrorBase code="504">
3
+ <template #text>
4
+ <h1 class="error-section__title">{{ $t('errors.504.title') }}</h1><br/>
5
+ <p class="error-section__description">{{ $t('errors.504.description.text_1') }}</p>
6
+ <p class="error-section__description">{{ $t('errors.504.description.text_2') }}</p>
7
+ </template>
8
+ </MgErrorBase>
9
+ </template>
@@ -0,0 +1,109 @@
1
+ <script setup lang="ts">
2
+ const props = defineProps<{
3
+ code: string; // "401" | "403" | "404" | "500" | "503" | "504"
4
+ }>();
5
+
6
+ const locale = useNuxtApp().$i18n.locale;
7
+ const colorMode = useColorMode();
8
+
9
+ type Theme = "dark" | "light";
10
+
11
+ const assets = computed(() => {
12
+ const theme: Theme =
13
+ colorMode.preference === "light" ? "light" : "dark";
14
+ return {
15
+ cover: `/imgs/errors/error-${props.code}-${theme}-cover.svg`,
16
+ image: `/imgs/errors/error-${props.code}-${theme}.svg`,
17
+ logo: theme === "dark" ? "/imgs/logo.svg" : "/imgs/logo-dark.svg",
18
+ };
19
+ });
20
+ </script>
21
+
22
+ <template>
23
+ <div class="container">
24
+ <section
25
+ class="d-flex flex-column flex-md-row flex-sm-column error-section justify-content-center position-relative"
26
+ >
27
+ <div class="error-section__content">
28
+ <div class="d-flex justify-center">
29
+ <img class="mx-auto" :src="assets.logo" alt="Logo" />
30
+ </div>
31
+ <img :src="assets.image" :alt="`Error ${code}`" />
32
+ <div>
33
+ <slot name="text">
34
+ <p class="error-section__title">{{ $t(`errors.${code}.title`) }}</p>
35
+ <br />
36
+ <p class="error-section__description">
37
+ {{ $t(`errors.${code}.description`) }}
38
+ </p>
39
+ </slot>
40
+ </div>
41
+ <NuxtLink
42
+ class="btn btn-primary px-5 btn-color"
43
+ :to="`/${locale}`"
44
+ >
45
+ {{ $t("errors.go_home") }}
46
+ </NuxtLink>
47
+ </div>
48
+ <img
49
+ class="error-section__cover--image-mobile d-md-none position-absolute"
50
+ :src="assets.cover"
51
+ aria-hidden="true"
52
+ />
53
+ <img
54
+ class="error-section__cover--image d-none d-md-block mt-5"
55
+ :src="assets.cover"
56
+ aria-hidden="true"
57
+ />
58
+ </section>
59
+
60
+ <!-- Slot for platform-specific extra content (news, games, banners etc.) -->
61
+ <slot name="extra" />
62
+ </div>
63
+ </template>
64
+
65
+ <style scoped lang="scss">
66
+ .btn-color {
67
+ background-color: var(--header-active-fg, var(--highlight-color, #ff7700));
68
+ color: var(--section-title-fg, #fff);
69
+ }
70
+
71
+ .container > * + * {
72
+ padding: 1rem 0;
73
+ }
74
+
75
+ .error-section {
76
+ gap: 5rem;
77
+
78
+ &__content {
79
+ margin-bottom: 4rem;
80
+ max-width: 20rem;
81
+
82
+ & > * + * {
83
+ padding: 1rem 0;
84
+ }
85
+ }
86
+
87
+ &__cover--image-mobile {
88
+ height: 10rem;
89
+ right: 0;
90
+ top: 9rem;
91
+ transform: translate(50%, 0);
92
+ }
93
+
94
+ &__cover--image {
95
+ height: 20rem;
96
+ }
97
+
98
+ &__title {
99
+ font-size: 28px;
100
+ font-weight: 600;
101
+ color: var(--section-title-fg, var(--card-cover-title));
102
+ }
103
+
104
+ &__description {
105
+ font-size: 12px;
106
+ color: var(--section-title-fg, var(--inactive));
107
+ }
108
+ }
109
+ </style>
@@ -0,0 +1,230 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * ErrorNewsCTA — Fetches trending articles from the Community platform
4
+ * and displays them as a CTA on error pages across all MGN platforms.
5
+ *
6
+ * Drives cross-platform traffic to mundogamer.community on error pages
7
+ * instead of dead ends.
8
+ */
9
+
10
+ interface FeedItem {
11
+ id: number;
12
+ slug: string;
13
+ title: string;
14
+ url_image_src?: string;
15
+ url_image?: string;
16
+ content_type?: "pulse" | "review" | "video" | string;
17
+ author?: { nickname?: string; name?: string };
18
+ created_at_diff?: string;
19
+ }
20
+
21
+ const props = defineProps({
22
+ limit: { type: Number, default: 4 },
23
+ });
24
+
25
+ const config = useRuntimeConfig();
26
+ const locale = useNuxtApp().$i18n?.locale?.value || "en";
27
+
28
+ // Community API base — every platform exposes VITE_BASE_URL_COMMUNITY.
29
+ // Community routes use /{locale}/feed/trending (no /api/v1 prefix —
30
+ // the community backend uses {locale_prefix} directly as the route prefix).
31
+ const communityApiBase = computed(() => {
32
+ const raw =
33
+ config.public.mgSharedUi?.communityApiUrl ||
34
+ config.public.communityApiUrl ||
35
+ import.meta.env.VITE_BASE_URL_COMMUNITY ||
36
+ "https://api.mundogamer.community";
37
+ return raw.replace(/\/$/, "");
38
+ });
39
+
40
+ // Community site URL for article links
41
+ const communitySiteUrl = computed(() => {
42
+ return (
43
+ config.public.mgSharedUi?.communitySiteUrl ||
44
+ config.public.communitySiteUrl ||
45
+ import.meta.env.VITE_COMMUNITY_SITE_URL ||
46
+ "https://mundogamer.community"
47
+ );
48
+ });
49
+
50
+ const articles = ref<FeedItem[]>([]);
51
+ const loading = ref(true);
52
+ const failed = ref(false);
53
+
54
+ const articlePath = (item: FeedItem): string => {
55
+ const typeMap: Record<string, string> = {
56
+ pulse: "articles",
57
+ review: "reviews",
58
+ video: "videos",
59
+ };
60
+ const type = typeMap[item.content_type ?? "pulse"] ?? "articles";
61
+ return `${communitySiteUrl.value}/${locale}/${type}/${item.slug}`;
62
+ };
63
+
64
+ const imageUrl = (item: FeedItem): string =>
65
+ item.url_image_src || item.url_image || "/imgs/default/no_img_large_dark.png";
66
+
67
+ onMounted(async () => {
68
+ try {
69
+ const res = await fetch(
70
+ `${communityApiBase.value}/${locale}/feed/trending?per_page=${props.limit}&page=1&join=false&order=desc`,
71
+ {
72
+ credentials: "omit",
73
+ // Without this, the community web route returns HTML/Inertia instead of JSON.
74
+ headers: { Accept: "application/json" },
75
+ },
76
+ );
77
+ if (!res.ok) { failed.value = true; return; }
78
+ const data = await res.json();
79
+ const items: FeedItem[] = data?.data?.data ?? data?.data ?? [];
80
+ articles.value = items.slice(0, props.limit);
81
+ } catch {
82
+ failed.value = true;
83
+ } finally {
84
+ loading.value = false;
85
+ }
86
+ });
87
+ </script>
88
+
89
+ <template>
90
+ <section v-if="!failed && (loading || articles.length)" class="error-news-cta">
91
+ <div class="error-news-cta__header">
92
+ <h4 class="error-news-cta__title">{{ $t("errors.news_cta_title") }}</h4>
93
+ <a
94
+ :href="`${communitySiteUrl}/${locale}`"
95
+ target="_blank"
96
+ class="error-news-cta__see-all"
97
+ >
98
+ {{ $t("errors.news_cta_see_all") }} →
99
+ </a>
100
+ </div>
101
+
102
+ <!-- Loading skeleton -->
103
+ <div v-if="loading" class="error-news-cta__grid">
104
+ <div v-for="n in limit" :key="n" class="error-news-cta__skeleton" />
105
+ </div>
106
+
107
+ <!-- Articles -->
108
+ <div v-else class="error-news-cta__grid">
109
+ <a
110
+ v-for="item in articles"
111
+ :key="item.id"
112
+ :href="articlePath(item)"
113
+ target="_blank"
114
+ class="error-news-cta__card"
115
+ >
116
+ <div
117
+ class="error-news-cta__card-img"
118
+ :style="{ backgroundImage: `url('${imageUrl(item)}')` }"
119
+ />
120
+ <div class="error-news-cta__card-body">
121
+ <span v-if="item.content_type" class="error-news-cta__card-tag">
122
+ {{ item.content_type }}
123
+ </span>
124
+ <p class="error-news-cta__card-title">{{ item.title }}</p>
125
+ <span v-if="item.created_at_diff" class="error-news-cta__card-date">
126
+ {{ item.created_at_diff }}
127
+ </span>
128
+ </div>
129
+ </a>
130
+ </div>
131
+ </section>
132
+ </template>
133
+
134
+ <style scoped lang="scss">
135
+ .error-news-cta {
136
+ padding: 2rem 0;
137
+
138
+ &__header {
139
+ display: flex;
140
+ align-items: center;
141
+ justify-content: space-between;
142
+ margin-bottom: 1.25rem;
143
+ }
144
+
145
+ &__title {
146
+ font-size: 1.1rem;
147
+ font-weight: 700;
148
+ color: var(--card-cover-title, #fff);
149
+ margin: 0;
150
+ }
151
+
152
+ &__see-all {
153
+ font-size: 0.8rem;
154
+ color: var(--highlight-color, #ff7700);
155
+ text-decoration: none;
156
+ white-space: nowrap;
157
+ &:hover { text-decoration: underline; }
158
+ }
159
+
160
+ &__grid {
161
+ display: grid;
162
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
163
+ gap: 1rem;
164
+
165
+ @media (max-width: 576px) {
166
+ grid-template-columns: 1fr;
167
+ }
168
+ }
169
+
170
+ &__skeleton {
171
+ height: 200px;
172
+ background: var(--body-bg-card, #1a1a1a);
173
+ animation: pulse 1.5s ease-in-out infinite;
174
+ }
175
+
176
+ &__card {
177
+ display: flex;
178
+ flex-direction: column;
179
+ background: var(--body-bg-card, #1a1a1a);
180
+ text-decoration: none;
181
+ transition: opacity 0.2s;
182
+
183
+ &:hover { opacity: 0.85; }
184
+
185
+ &-img {
186
+ height: 140px;
187
+ background-size: cover;
188
+ background-position: center;
189
+ background-color: var(--body-bg, #111);
190
+ }
191
+
192
+ &-body {
193
+ padding: 0.75rem;
194
+ display: flex;
195
+ flex-direction: column;
196
+ gap: 0.3rem;
197
+ }
198
+
199
+ &-tag {
200
+ font-size: 0.65rem;
201
+ font-weight: 700;
202
+ text-transform: uppercase;
203
+ letter-spacing: 0.05em;
204
+ color: var(--highlight-color, #ff7700);
205
+ }
206
+
207
+ &-title {
208
+ font-size: 0.82rem;
209
+ font-weight: 600;
210
+ color: var(--card-cover-title, #fff);
211
+ line-height: 1.3;
212
+ margin: 0;
213
+ display: -webkit-box;
214
+ -webkit-line-clamp: 3;
215
+ -webkit-box-orient: vertical;
216
+ overflow: hidden;
217
+ }
218
+
219
+ &-date {
220
+ font-size: 0.7rem;
221
+ color: var(--inactive, #888);
222
+ }
223
+ }
224
+
225
+ @keyframes pulse {
226
+ 0%, 100% { opacity: 1; }
227
+ 50% { opacity: 0.4; }
228
+ }
229
+ }
230
+ </style>
@@ -1,23 +1,33 @@
1
1
  <script setup lang="ts">
2
2
  import { storeToRefs } from "pinia";
3
+ import { buildRegisterUrl } from "../../utils/authRedirect";
3
4
 
4
5
  const loginStore = useLoginStore();
5
6
  const runtimeConfig = useRuntimeConfig();
6
7
  const authStore = useAuthStore();
7
8
  const { signedIn } = storeToRefs(authStore);
8
- const accountsBaseUrl = runtimeConfig.public.mgSharedUi?.accountsBaseUrl || runtimeConfig.public.accountsBaseUrl;
9
+ const accountsBaseUrl =
10
+ runtimeConfig.public.mgSharedUi?.accountsBaseUrl || runtimeConfig.public.accountsBaseUrl || "";
11
+ const systemId =
12
+ runtimeConfig.public.mgSharedUi?.systemId ||
13
+ runtimeConfig.public.platformId ||
14
+ import.meta.env.VITE_SYSTEM_ID ||
15
+ "";
9
16
 
10
- defineProps({
17
+ const props = defineProps({
11
18
  open: {
12
19
  type: Boolean,
13
20
  default: false,
14
21
  },
22
+ loginUrl: {
23
+ type: String,
24
+ default: "",
25
+ },
15
26
  });
16
27
 
17
- const registerUrl = computed(() => {
18
- const current = typeof window !== "undefined" ? window.location.href : "";
19
- return `${accountsBaseUrl}/register?redirect_to=${current}`;
20
- });
28
+ const registerUrl = computed(() =>
29
+ buildRegisterUrl(accountsBaseUrl, undefined, systemId),
30
+ );
21
31
 
22
32
  function close() {
23
33
  loginStore.toggleLoginModalComponent();
@@ -40,6 +50,9 @@ function close() {
40
50
  {{ $t("more.login_modal.subtitle") }}
41
51
  </div>
42
52
  <div class="buttons">
53
+ <a v-if="props.loginUrl" class="btn1" :href="props.loginUrl">
54
+ {{ $t("more.login_modal.btn_login") }}
55
+ </a>
43
56
  <a v-if="!signedIn" class="btn1" :href="registerUrl">
44
57
  {{ $t("more.login_modal.btn_1") }}
45
58
  </a>
@@ -0,0 +1,132 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * MgLoginRegisterMenu — standardized guest dropdown for the header user icon.
4
+ *
5
+ * Shows a prominent Login button + "Don't have an account yet? Register" link,
6
+ * fully themed via platform CSS variables (academy orange, tv cyan, etc.).
7
+ *
8
+ * Redirect rules are baked in and correct for every platform:
9
+ * - Login → the platform's own login endpoint (apiBaseURL host) with
10
+ * redirect_to = current page, so the user RETURNS here after login.
11
+ * - Register → accountsBaseUrl/register with redirect_to + system_id.
12
+ *
13
+ * Usage (header guest state): place inside a `position: relative` parent
14
+ * <li class="menu-item" style="position: relative">
15
+ * <MgLoginRegisterMenu v-if="!signedIn && open" />
16
+ * </li>
17
+ *
18
+ * Props let a platform override the login base or disable dropdown positioning.
19
+ */
20
+ import { buildLoginUrl, buildRegisterUrl } from "../../utils/authRedirect";
21
+
22
+ const props = defineProps({
23
+ // Override the login URL entirely (else derived from apiBaseURL host).
24
+ loginUrl: { type: String, default: "" },
25
+ // When false, renders as an inline card instead of an absolute dropdown.
26
+ dropdown: { type: Boolean, default: true },
27
+ });
28
+
29
+ const runtimeConfig = useRuntimeConfig();
30
+
31
+ const apiBase = (
32
+ (runtimeConfig.public.mgSharedUi?.apiBaseURL as string) ||
33
+ (runtimeConfig.public.apiBaseURL as string) ||
34
+ (import.meta.env.VITE_API_BASE_URL as string) ||
35
+ ""
36
+ ).replace(/\/api\/v1\/?$/, "");
37
+
38
+ const accountsBaseUrl =
39
+ runtimeConfig.public.mgSharedUi?.accountsBaseUrl ||
40
+ runtimeConfig.public.accountsBaseUrl ||
41
+ import.meta.env.VITE_BASE_ACCOUNTS_URL ||
42
+ "";
43
+
44
+ const systemId =
45
+ runtimeConfig.public.mgSharedUi?.systemId ||
46
+ runtimeConfig.public.platformId ||
47
+ import.meta.env.VITE_SYSTEM_ID ||
48
+ "";
49
+
50
+ // Computed at click time so redirect_to is always the CURRENT url.
51
+ function goLogin() {
52
+ if (typeof window === "undefined") return;
53
+ window.location.href = props.loginUrl || buildLoginUrl(apiBase);
54
+ }
55
+ function goRegister() {
56
+ if (typeof window === "undefined") return;
57
+ window.location.href = buildRegisterUrl(accountsBaseUrl, undefined, systemId);
58
+ }
59
+ </script>
60
+
61
+ <template>
62
+ <div class="mg-login-register" :class="{ 'mg-login-register--dropdown': dropdown }">
63
+ <button type="button" class="mg-login-register__login" @click="goLogin">
64
+ {{ $t("header.login.btn_login") }}
65
+ </button>
66
+ <p class="mg-login-register__register">
67
+ {{ $t("header.login.no_account") }}
68
+ <a class="mg-login-register__register-link" @click.prevent="goRegister">
69
+ {{ $t("header.login.btn_register") }}
70
+ </a>
71
+ </p>
72
+ </div>
73
+ </template>
74
+
75
+ <style scoped lang="scss">
76
+ .mg-login-register {
77
+ display: flex;
78
+ flex-direction: column;
79
+ gap: 0.85rem;
80
+ padding: 1.25rem;
81
+ width: 300px;
82
+ max-width: 300px;
83
+ background: var(--header-ui-config-bg, var(--sidebar-bg, var(--body-bg-card, #1c1c1c)));
84
+ border: 1px solid var(--header-ui-config-border, var(--border-card, rgba(255, 255, 255, 0.08)));
85
+
86
+ &--dropdown {
87
+ position: absolute;
88
+ top: 100%;
89
+ right: 0;
90
+ margin-top: 8px;
91
+ z-index: 9999;
92
+ }
93
+
94
+ &__login {
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: center;
98
+ width: 100%;
99
+ min-height: 44px;
100
+ padding: 10px 16px;
101
+ font-size: 15px;
102
+ font-weight: 600;
103
+ border: none;
104
+ cursor: pointer;
105
+ white-space: nowrap;
106
+ text-decoration: none;
107
+ // Platform theme highlight + readable contrast text.
108
+ background: var(--bt-active, var(--header-active-fg, var(--highlight-color, #f70)));
109
+ color: var(--header-active-bg, #18181a);
110
+ transition: opacity 0.15s;
111
+
112
+ &:hover { opacity: 0.9; }
113
+ }
114
+
115
+ &__register {
116
+ margin: 0;
117
+ text-align: center;
118
+ font-size: 13px;
119
+ color: var(--sidebar-menus-fg, var(--inactive, #aaa));
120
+ }
121
+
122
+ &__register-link {
123
+ color: var(--bt-active, var(--header-active-fg, var(--highlight-color, #f70)));
124
+ font-weight: 600;
125
+ text-decoration: none;
126
+ cursor: pointer;
127
+ margin-left: 4px;
128
+
129
+ &:hover { text-decoration: underline; }
130
+ }
131
+ }
132
+ </style>
@@ -0,0 +1,62 @@
1
+ <script setup lang="ts">
2
+ import { useTestimoniesStore } from "../../stores/testimonies";
3
+
4
+ const props = defineProps({
5
+ params: {
6
+ type: Object,
7
+ default: () => ({}),
8
+ },
9
+ autoLoad: {
10
+ type: Boolean,
11
+ default: true,
12
+ },
13
+ });
14
+
15
+ const store = useTestimoniesStore();
16
+ const { testimonies, loading, error } = storeToRefs(store);
17
+
18
+ onMounted(() => {
19
+ if (props.autoLoad) store.fetchTestimonies(props.params);
20
+ });
21
+ </script>
22
+
23
+ <template>
24
+ <div class="mg-testimonials">
25
+ <div v-if="loading" class="mg-testimonials__loading">
26
+ <slot name="loading" />
27
+ </div>
28
+ <div v-else-if="error" class="mg-testimonials__error">
29
+ <slot name="error" :error="error" />
30
+ </div>
31
+ <div v-else-if="testimonies.length === 0" class="mg-testimonials__empty">
32
+ <slot name="empty" />
33
+ </div>
34
+ <div v-else class="mg-testimonials__list">
35
+ <slot :testimonies="testimonies">
36
+ <div
37
+ v-for="(testimony, i) in testimonies"
38
+ :key="testimony.id ?? i"
39
+ class="mg-testimonials__item"
40
+ >
41
+ <blockquote class="mg-testimonials__item__body">
42
+ {{ testimony.body }}
43
+ </blockquote>
44
+ <div class="mg-testimonials__item__author">
45
+ <img
46
+ v-if="testimony.avatar_url"
47
+ :src="testimony.avatar_url"
48
+ :alt="testimony.name"
49
+ class="mg-testimonials__item__avatar"
50
+ />
51
+ <div class="mg-testimonials__item__meta">
52
+ <span class="mg-testimonials__item__name">{{ testimony.name }}</span>
53
+ <span v-if="testimony.role || testimony.company" class="mg-testimonials__item__role">
54
+ {{ [testimony.role, testimony.company].filter(Boolean).join(" · ") }}
55
+ </span>
56
+ </div>
57
+ </div>
58
+ </div>
59
+ </slot>
60
+ </div>
61
+ </div>
62
+ </template>