@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.
- 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/MgLoginRegisterMenu.vue +132 -0
- 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 +2 -2
- 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,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>
|