@mundogamernetwork/shared-ui 1.13.2 → 1.13.4
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.
|
@@ -32,6 +32,27 @@ const emit = defineEmits<{ (e: "navigate"): void }>();
|
|
|
32
32
|
/** Missions and the reward store live on Community for the whole ecosystem. */
|
|
33
33
|
const COMMUNITY_URL = "https://mundogamer.community";
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Dig out the payload regardless of how many envelopes wrap it.
|
|
37
|
+
*
|
|
38
|
+
* The API nests deeper than its own sendResponse() suggests — the gateway adds
|
|
39
|
+
* a second {message,data,status_code} layer on top, so the payload sits at
|
|
40
|
+
* response.data.data.data rather than response.data.data. Reading a fixed
|
|
41
|
+
* depth silently returned the inner envelope, which has no `level`, so the bar
|
|
42
|
+
* loaded and vanished on a perfectly good 200. Walking down until the shape
|
|
43
|
+
* appears survives either nesting.
|
|
44
|
+
*/
|
|
45
|
+
function unwrapSummary(payload: any): GamificationSummary | null {
|
|
46
|
+
let node = payload;
|
|
47
|
+
|
|
48
|
+
for (let depth = 0; depth < 4 && node && typeof node === "object"; depth += 1) {
|
|
49
|
+
if (node.level || node.wallet || node.missions) return node as GamificationSummary;
|
|
50
|
+
node = node.data;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
35
56
|
const summary = ref<GamificationSummary | null>(null);
|
|
36
57
|
const loaded = ref(false);
|
|
37
58
|
|
|
@@ -55,7 +76,12 @@ const destination = computed(() => {
|
|
|
55
76
|
const balance = computed(() => {
|
|
56
77
|
const fromHost = props.mgcBalance;
|
|
57
78
|
const value = fromHost !== null && fromHost !== undefined ? fromHost : summary.value?.wallet?.balance;
|
|
58
|
-
|
|
79
|
+
// The API sends the balance as a decimal string ("1801.50"), not a number.
|
|
80
|
+
const numeric = typeof value === "string" ? Number(value) : value;
|
|
81
|
+
|
|
82
|
+
return typeof numeric === "number" && Number.isFinite(numeric)
|
|
83
|
+
? Math.floor(numeric).toLocaleString()
|
|
84
|
+
: null;
|
|
59
85
|
});
|
|
60
86
|
|
|
61
87
|
// progress_percent comes from the API; the local computation is a fallback for
|
|
@@ -71,7 +97,7 @@ const percent = computed(() => {
|
|
|
71
97
|
onMounted(async () => {
|
|
72
98
|
try {
|
|
73
99
|
const response = await fetchGamificationSummary();
|
|
74
|
-
summary.value = response?.data
|
|
100
|
+
summary.value = unwrapSummary(response?.data);
|
|
75
101
|
} catch (error) {
|
|
76
102
|
// Never let a gamification hiccup break the header it sits in — but say
|
|
77
103
|
// so. Swallowing this silently made a deployed bar that renders nothing
|
|
@@ -86,13 +112,24 @@ onMounted(async () => {
|
|
|
86
112
|
</script>
|
|
87
113
|
|
|
88
114
|
<template>
|
|
115
|
+
<!--
|
|
116
|
+
The root renders unconditionally so the server and the client agree on
|
|
117
|
+
the node at hydration time. A v-if here produced a comment on the server
|
|
118
|
+
and an <a> on the client, and Vue bails out of hydrating a subtree whose
|
|
119
|
+
node type disagrees — leaving the server markup in place and never
|
|
120
|
+
mounting the component, so onMounted never ran and the bar was silently
|
|
121
|
+
dead. The skeleton is what both sides render before the fetch resolves.
|
|
122
|
+
-->
|
|
89
123
|
<a
|
|
90
|
-
v-if="loaded && level"
|
|
91
124
|
:href="destination"
|
|
92
125
|
class="mg-xp-bar"
|
|
93
|
-
:class="{
|
|
126
|
+
:class="{
|
|
127
|
+
'mg-xp-bar--compact': props.compact,
|
|
128
|
+
'mg-xp-bar--empty': loaded && !level,
|
|
129
|
+
}"
|
|
94
130
|
@click="emit('navigate')"
|
|
95
131
|
>
|
|
132
|
+
<template v-if="level">
|
|
96
133
|
<div class="mg-xp-bar__top">
|
|
97
134
|
<span class="mg-xp-bar__badge">{{ level.current_level }}</span>
|
|
98
135
|
|
|
@@ -111,6 +148,17 @@ onMounted(async () => {
|
|
|
111
148
|
<div class="mg-xp-bar__track">
|
|
112
149
|
<div class="mg-xp-bar__fill" :style="{ width: percent + '%' }" />
|
|
113
150
|
</div>
|
|
151
|
+
</template>
|
|
152
|
+
|
|
153
|
+
<template v-else>
|
|
154
|
+
<div class="mg-xp-bar__top">
|
|
155
|
+
<span class="mg-xp-bar__badge mg-xp-bar__skeleton" />
|
|
156
|
+
<div class="mg-xp-bar__meta">
|
|
157
|
+
<span class="mg-xp-bar__skeleton mg-xp-bar__skeleton--line" />
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="mg-xp-bar__track" />
|
|
161
|
+
</template>
|
|
114
162
|
</a>
|
|
115
163
|
</template>
|
|
116
164
|
|
|
@@ -232,6 +280,22 @@ onMounted(async () => {
|
|
|
232
280
|
transition: width 0.3s ease;
|
|
233
281
|
}
|
|
234
282
|
|
|
283
|
+
// Hidden only after the fetch resolves with nothing — a client-side change
|
|
284
|
+
// that happens well after hydration, so it cannot cause a mismatch.
|
|
285
|
+
&--empty {
|
|
286
|
+
display: none;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
&__skeleton {
|
|
290
|
+
background: rgba(128, 128, 128, 0.25);
|
|
291
|
+
border-color: transparent;
|
|
292
|
+
|
|
293
|
+
&--line {
|
|
294
|
+
height: 9px;
|
|
295
|
+
width: 70%;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
235
299
|
&--compact {
|
|
236
300
|
padding: 0.35rem 0.5rem;
|
|
237
301
|
}
|
package/package.json
CHANGED
|
@@ -18,7 +18,8 @@ export interface XpLevel {
|
|
|
18
18
|
|
|
19
19
|
export interface GamificationSummary {
|
|
20
20
|
level: XpLevel;
|
|
21
|
-
|
|
21
|
+
/** The API serialises these as decimal strings, not numbers. */
|
|
22
|
+
wallet: { balance: number | string; lifetime_earned: number | string };
|
|
22
23
|
missions: { active: any[]; claimable_count: number };
|
|
23
24
|
achievements: { total_unlocked: number; recent: any[] };
|
|
24
25
|
}
|