@mundogamernetwork/shared-ui 1.0.3 → 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.
- package/components/errors/401.vue +9 -0
- package/components/errors/403.vue +9 -0
- package/components/errors/404.vue +13 -0
- package/components/errors/500.vue +9 -0
- package/components/errors/503.vue +9 -0
- package/components/errors/504.vue +9 -0
- package/components/errors/MgErrorBase.vue +109 -0
- package/components/errors/MgErrorNewsCta.vue +230 -0
- package/components/ui/MgLoginModal.vue +19 -6
- package/components/ui/MgTestimonials.vue +62 -0
- package/components/ui/MgWalletPlanBadge.vue +95 -0
- package/composables/useConfirmation.ts +117 -0
- package/composables/useForceLogout.ts +73 -0
- package/composables/usePaymentListener.ts +81 -0
- package/error.vue +59 -112
- package/package.json +1 -1
- package/services/httpService.ts +46 -4
- package/services/testimoniesService.ts +40 -0
- package/stores/chat.ts +22 -0
- package/stores/notifications.ts +39 -1
- package/stores/testimonies.ts +40 -0
- package/utils/authRedirect.ts +18 -0
- package/utils/clearSession.ts +103 -0
|
@@ -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 =
|
|
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
|
-
|
|
19
|
-
|
|
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>
|