@mundogamernetwork/shared-ui 1.0.2 → 1.1.0

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,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>
@@ -0,0 +1,95 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * MgWalletPlanBadge — shows the user's MGC balance + current plan in the nav header.
4
+ *
5
+ * Reads straight from the auth store, which every service-api enriches on
6
+ * /auth/user with `mgc_balance` and `subscription` ({ plan_name, plan_slug,
7
+ * tier, is_subscriber }). No extra API call — never hit /gamification/wallet
8
+ * from the header.
9
+ *
10
+ * Drop <MgWalletPlanBadge /> into any header. Props let each platform tweak
11
+ * the plan-slug prefix it strips (e.g. "academy-", "tv-").
12
+ */
13
+ import { storeToRefs } from "pinia";
14
+
15
+ const props = defineProps({
16
+ // Strip this platform prefix from a plan slug fallback (e.g. "academy-pro" → "pro")
17
+ planPrefix: { type: String, default: "" },
18
+ showPlan: { type: Boolean, default: true },
19
+ showWallet: { type: Boolean, default: true },
20
+ });
21
+
22
+ const authStore = useAuthStore();
23
+ const { user } = storeToRefs(authStore);
24
+
25
+ const walletBalance = computed<number>(
26
+ () => (user.value as any)?.mgc_balance ?? (user.value as any)?.coins ?? 0,
27
+ );
28
+
29
+ const planDisplayName = computed(() => {
30
+ const sub = (user.value as any)?.subscription ?? {};
31
+ let raw: string = sub.plan_name || sub.plan_slug || "";
32
+ if (!sub.plan_name && raw && props.planPrefix) {
33
+ raw = raw.replace(new RegExp(`^${props.planPrefix}`), "");
34
+ }
35
+ raw = (raw || "Free").replace(/-/g, " ");
36
+ return raw.charAt(0).toUpperCase() + raw.slice(1);
37
+ });
38
+
39
+ const isSubscriber = computed(() => !!(user.value as any)?.subscription?.is_subscriber);
40
+ </script>
41
+
42
+ <template>
43
+ <div class="mg-wallet-plan">
44
+ <span v-if="showWallet" class="mg-wallet-plan__coins">
45
+ <MGIcon icon="coin" />
46
+ {{ walletBalance.toLocaleString() }}
47
+ <span class="mg-wallet-plan__currency">MGC</span>
48
+ </span>
49
+ <span
50
+ v-if="showPlan"
51
+ class="mg-wallet-plan__tag"
52
+ :class="{ 'is-subscriber': isSubscriber }"
53
+ >
54
+ {{ planDisplayName }}
55
+ </span>
56
+ </div>
57
+ </template>
58
+
59
+ <style scoped lang="scss">
60
+ .mg-wallet-plan {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ gap: 0.6rem;
64
+
65
+ &__coins {
66
+ display: inline-flex;
67
+ align-items: center;
68
+ gap: 0.3rem;
69
+ font-size: 0.85rem;
70
+ font-weight: 600;
71
+ color: var(--active, var(--card-cover-title, #fff));
72
+ }
73
+
74
+ &__currency {
75
+ font-size: 0.7rem;
76
+ opacity: 0.7;
77
+ font-weight: 500;
78
+ }
79
+
80
+ &__tag {
81
+ font-size: 0.7rem;
82
+ font-weight: 600;
83
+ text-transform: uppercase;
84
+ letter-spacing: 0.04em;
85
+ padding: 2px 8px;
86
+ color: var(--inactive, #aaa);
87
+ border: 1px solid var(--border-card, #333);
88
+
89
+ &.is-subscriber {
90
+ color: var(--highlight-color, var(--header-active-fg, #ff7700));
91
+ border-color: currentColor;
92
+ }
93
+ }
94
+ }
95
+ </style>