@mundogamernetwork/shared-ui 1.1.44 → 1.1.46

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,550 @@
1
+ <script setup lang="ts">
2
+ import {
3
+ fetchAvailablePools,
4
+ fetchMyRequests,
5
+ fetchPlatforms,
6
+ requestKey as apiRequestKey,
7
+ revealKey as apiRevealKey,
8
+ type KeyPool,
9
+ type KeyRequest,
10
+ type RevealResult,
11
+ } from '../../services/keyService'
12
+ import { mgApiUrl } from '../../services/httpService'
13
+
14
+ const { t } = useI18n()
15
+
16
+ const props = defineProps<{
17
+ requesterContext: 'media_partner' | 'campaign_creator' | 'user'
18
+ accessToken?: string
19
+ showcaseId?: number
20
+ contextType?: string
21
+ contextId?: number
22
+ gameId?: number
23
+ /** If set, the browser will redirect here on 401 instead of building the URL automatically */
24
+ loginUrl?: string
25
+ }>()
26
+
27
+ const emit = defineEmits<{
28
+ 'pools-ready': [count: number]
29
+ }>()
30
+
31
+ // ─── State ────────────────────────────────────────────────────────────────────
32
+
33
+ const pools = ref<KeyPool[]>([])
34
+ const myRequests = ref<KeyRequest[]>([])
35
+ const platforms = ref<{ id: number; name: string }[]>([])
36
+ const loadingPools = ref(true)
37
+ const loadingRequests = ref(true)
38
+ const requesting = ref<number | null>(null)
39
+ const activeTab = ref<'available' | 'mine'>('available')
40
+ const isAuthenticated = ref(true) // assume true; set false on 401
41
+
42
+ const requestForms = ref<Record<number, { platform_ids: number[]; description: string }>>({})
43
+ const revealingId = ref<number | null>(null)
44
+ const revealResult = ref<RevealResult | null>(null)
45
+
46
+ // ─── Params ───────────────────────────────────────────────────────────────────
47
+
48
+ function queryParams(): Record<string, any> {
49
+ const p: Record<string, any> = {}
50
+ if (props.accessToken) p.access_token = props.accessToken
51
+ if (props.showcaseId) p.showcase_id = props.showcaseId
52
+ if (props.contextType) p.context = props.contextType
53
+ if (props.contextId) p.context_id = props.contextId
54
+ if (props.gameId) p.game_id = props.gameId
55
+ if (!props.contextType && !props.gameId) p['filter[mg_platform]'] = 'MGAG'
56
+ return p
57
+ }
58
+
59
+ // ─── Load ─────────────────────────────────────────────────────────────────────
60
+
61
+ async function loadPools() {
62
+ loadingPools.value = true
63
+ try {
64
+ const res = await fetchAvailablePools(queryParams())
65
+ pools.value = (res as any)?.data ?? []
66
+ pools.value.forEach(p => {
67
+ if (!requestForms.value[p.id]) {
68
+ requestForms.value[p.id] = { platform_ids: [], description: '' }
69
+ }
70
+ })
71
+ emit('pools-ready', pools.value.length)
72
+ } catch (err: any) {
73
+ if (err?.response?.status === 401) isAuthenticated.value = false
74
+ emit('pools-ready', 0)
75
+ } finally {
76
+ loadingPools.value = false
77
+ }
78
+ }
79
+
80
+ async function loadMyRequests() {
81
+ loadingRequests.value = true
82
+ try {
83
+ const res = await fetchMyRequests()
84
+ myRequests.value = (res as any)?.data ?? []
85
+ } catch (err: any) {
86
+ if (err?.response?.status === 401) isAuthenticated.value = false
87
+ } finally {
88
+ loadingRequests.value = false
89
+ }
90
+ }
91
+
92
+ async function loadPlatformsData() {
93
+ try {
94
+ const res = await fetchPlatforms()
95
+ platforms.value = (res as any)?.data ?? []
96
+ } catch {}
97
+ }
98
+
99
+ onMounted(async () => {
100
+ await Promise.all([loadPools(), loadMyRequests(), loadPlatformsData()])
101
+ })
102
+
103
+ // ─── Auth ─────────────────────────────────────────────────────────────────────
104
+
105
+ function redirectToLogin() {
106
+ const url = props.loginUrl ?? mgApiUrl('login') + '?redirect_to=' + encodeURIComponent(window.location.href)
107
+ window.location.href = url
108
+ }
109
+
110
+ // ─── Request ──────────────────────────────────────────────────────────────────
111
+
112
+ async function doRequestKey(pool: KeyPool) {
113
+ requesting.value = pool.id
114
+ try {
115
+ const form = requestForms.value[pool.id] ?? { platform_ids: [], description: '' }
116
+ const body: Record<string, any> = {
117
+ platform_ids: form.platform_ids,
118
+ description: form.description || undefined,
119
+ }
120
+ if (props.accessToken) body.access_token = props.accessToken
121
+ await apiRequestKey(pool.id, body)
122
+ pool.already_requested = true
123
+ await loadMyRequests()
124
+ } catch (err: any) {
125
+ if (err?.response?.status === 401) {
126
+ isAuthenticated.value = false
127
+ }
128
+ } finally {
129
+ requesting.value = null
130
+ }
131
+ }
132
+
133
+ function handleRequestKey(pool: KeyPool) {
134
+ if (!isAuthenticated.value) {
135
+ redirectToLogin()
136
+ return
137
+ }
138
+ doRequestKey(pool)
139
+ }
140
+
141
+ // ─── Reveal ───────────────────────────────────────────────────────────────────
142
+
143
+ async function revealCode(reqId: number) {
144
+ revealingId.value = reqId
145
+ revealResult.value = null
146
+ try {
147
+ const body: Record<string, any> = {}
148
+ if (props.accessToken) body.access_token = props.accessToken
149
+ const res = await apiRevealKey(reqId, body)
150
+ revealResult.value = (res as any)?.data ?? null
151
+ } catch (err: any) {
152
+ if (err?.response?.status === 401) isAuthenticated.value = false
153
+ } finally {
154
+ revealingId.value = null
155
+ }
156
+ }
157
+
158
+ function closeReveal() {
159
+ revealResult.value = null
160
+ }
161
+
162
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
163
+
164
+ function statusColor(slug: string): string {
165
+ return { pending: 'orange', approved: 'green', refused: 'red', cancelled: 'gray' }[slug] ?? 'gray'
166
+ }
167
+ </script>
168
+
169
+ <template>
170
+ <div class="kb">
171
+ <!-- Auth wall -->
172
+ <div v-if="!isAuthenticated" class="kb__auth-wall">
173
+ <p>{{ $t('keys.browser.login_required') }}</p>
174
+ <button class="kb__login-btn" @click="redirectToLogin">
175
+ {{ $t('keys.browser.login_btn') }}
176
+ </button>
177
+ </div>
178
+
179
+ <template v-else>
180
+ <!-- Tabs -->
181
+ <div class="kb__tabs">
182
+ <button
183
+ class="kb__tab"
184
+ :class="{ 'kb__tab--active': activeTab === 'available' }"
185
+ @click="activeTab = 'available'"
186
+ >
187
+ {{ $t('keys.browser.tab_available') }}
188
+ <span class="kb__badge">{{ pools.filter(p => !p.already_requested && p.available_count > 0).length }}</span>
189
+ </button>
190
+ <button
191
+ class="kb__tab"
192
+ :class="{ 'kb__tab--active': activeTab === 'mine' }"
193
+ @click="activeTab = 'mine'"
194
+ >
195
+ {{ $t('keys.browser.tab_mine') }}
196
+ <span class="kb__badge">{{ myRequests.length }}</span>
197
+ </button>
198
+ </div>
199
+
200
+ <!-- Reveal result modal -->
201
+ <KeyRevealModal
202
+ v-if="revealResult"
203
+ :code="revealResult.code"
204
+ :platform="revealResult.platform"
205
+ :revealed-at="revealResult.revealed_at"
206
+ @close="closeReveal"
207
+ />
208
+
209
+ <!-- ── Available pools ── -->
210
+ <div v-if="activeTab === 'available'">
211
+ <div v-if="loadingPools" class="kb__loading">{{ $t('common.loading') }}</div>
212
+
213
+ <div v-else-if="pools.length === 0" class="kb__empty">
214
+ {{ $t('keys.browser.no_pools') }}
215
+ </div>
216
+
217
+ <div v-else class="kb__pool-grid">
218
+ <div
219
+ v-for="pool in pools"
220
+ :key="pool.id"
221
+ class="kb__pool-card"
222
+ :class="{ 'kb__pool-card--exhausted': pool.available_count === 0 }"
223
+ >
224
+ <div class="kb__pool-name">{{ pool.name }}</div>
225
+
226
+ <div class="kb__pool-availability">
227
+ <span v-if="pool.available_count > 0" class="kb__avail-ok">
228
+ {{ $t('keys.browser.keys_available', { n: pool.available_count }) }}
229
+ </span>
230
+ <span v-else class="kb__avail-empty">{{ $t('keys.browser.no_keys') }}</span>
231
+ </div>
232
+
233
+ <div v-if="!pool.already_requested && pool.available_count > 0 && platforms.length" class="kb__platform-sel">
234
+ <label class="kb__label">{{ $t('keys.browser.select_platform') }}</label>
235
+ <div class="kb__platform-chips">
236
+ <label
237
+ v-for="p in platforms"
238
+ :key="p.id"
239
+ class="kb__platform-chip"
240
+ :class="{ 'kb__platform-chip--active': requestForms[pool.id]?.platform_ids.includes(p.id) }"
241
+ >
242
+ <input
243
+ type="checkbox"
244
+ :value="p.id"
245
+ v-model="requestForms[pool.id].platform_ids"
246
+ style="display:none"
247
+ />
248
+ {{ p.name }}
249
+ </label>
250
+ </div>
251
+ </div>
252
+
253
+ <div v-if="!pool.already_requested && pool.available_count > 0" class="kb__description-field">
254
+ <label class="kb__label">{{ $t('keys.browser.description_label') }}</label>
255
+ <textarea
256
+ v-model="requestForms[pool.id].description"
257
+ class="kb__textarea"
258
+ :placeholder="$t('keys.browser.description_placeholder')"
259
+ rows="2"
260
+ />
261
+ </div>
262
+
263
+ <div class="kb__pool-footer">
264
+ <span v-if="pool.already_requested" class="kb__requested-label">
265
+ ✓ {{ $t('keys.browser.already_requested') }}
266
+ </span>
267
+ <button
268
+ v-else
269
+ class="kb__request-btn"
270
+ :disabled="pool.available_count === 0 || requesting === pool.id"
271
+ @click="handleRequestKey(pool)"
272
+ >
273
+ {{ requesting === pool.id ? $t('common.sending') : $t('keys.browser.request_btn') }}
274
+ </button>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ </div>
279
+
280
+ <!-- ── My requests ── -->
281
+ <div v-else-if="activeTab === 'mine'">
282
+ <div v-if="loadingRequests" class="kb__loading">{{ $t('common.loading') }}</div>
283
+
284
+ <div v-else-if="myRequests.length === 0" class="kb__empty">
285
+ {{ $t('keys.browser.no_requests') }}
286
+ </div>
287
+
288
+ <div v-else class="kb__request-list">
289
+ <div v-for="req in myRequests" :key="req.id" class="kb__request-row">
290
+ <div class="kb__request-info">
291
+ <span class="kb__request-pool">{{ req.pool_name }}</span>
292
+ <span v-if="req.platforms?.length" class="kb__request-platform">
293
+ {{ req.platforms.join(', ') }}
294
+ </span>
295
+ </div>
296
+
297
+ <span
298
+ class="kb__status-chip"
299
+ :style="{ '--chip-color': `var(--${statusColor(req.status_slug)})` }"
300
+ >
301
+ {{ req.status_name || req.status_slug }}
302
+ </span>
303
+
304
+ <div class="kb__request-action">
305
+ <span v-if="req.revealed_at" class="kb__revealed-label">
306
+ {{ $t('keys.browser.revealed') }}
307
+ </span>
308
+ <button
309
+ v-else-if="req.status_slug === 'approved'"
310
+ class="kb__reveal-btn"
311
+ :disabled="revealingId === req.id"
312
+ @click="revealCode(req.id)"
313
+ >
314
+ {{ revealingId === req.id ? $t('common.loading') : $t('keys.browser.reveal_btn') }}
315
+ </button>
316
+ <span v-else-if="req.status_slug === 'pending'" class="kb__pending-label">
317
+ {{ $t('keys.browser.pending_approval') }}
318
+ </span>
319
+ </div>
320
+ </div>
321
+ </div>
322
+ </div>
323
+ </template>
324
+ </div>
325
+ </template>
326
+
327
+ <style scoped lang="scss">
328
+ .kb {
329
+ &__auth-wall {
330
+ padding: 40px;
331
+ text-align: center;
332
+ color: var(--text-secondary, #888);
333
+ display: flex;
334
+ flex-direction: column;
335
+ align-items: center;
336
+ gap: 16px;
337
+ }
338
+
339
+ &__login-btn {
340
+ padding: 10px 24px;
341
+ background: var(--accent, #00d4ff);
342
+ color: #000;
343
+ border: none;
344
+ font-weight: 600;
345
+ font-size: 0.9rem;
346
+ cursor: pointer;
347
+ transition: opacity 0.15s;
348
+ &:hover { opacity: 0.85; }
349
+ }
350
+
351
+ &__tabs {
352
+ display: flex;
353
+ gap: 4px;
354
+ border-bottom: 1px solid var(--border-color, #2a2a2a);
355
+ margin-bottom: 20px;
356
+ }
357
+
358
+ &__tab {
359
+ padding: 10px 18px;
360
+ background: none;
361
+ border: none;
362
+ border-bottom: 2px solid transparent;
363
+ color: var(--text-secondary, #888);
364
+ cursor: pointer;
365
+ display: flex;
366
+ align-items: center;
367
+ gap: 6px;
368
+ font-size: 0.9rem;
369
+ transition: color 0.15s, border-color 0.15s;
370
+
371
+ &--active {
372
+ color: var(--text-primary, #fff);
373
+ border-bottom-color: var(--accent, #00d4ff);
374
+ }
375
+ }
376
+
377
+ &__badge {
378
+ background: var(--surface-raised, #1e1e1e);
379
+ border-radius: 10px;
380
+ padding: 2px 7px;
381
+ font-size: 0.75rem;
382
+ }
383
+
384
+ &__loading,
385
+ &__empty {
386
+ padding: 40px;
387
+ text-align: center;
388
+ color: var(--text-secondary, #888);
389
+ font-size: 0.9rem;
390
+ }
391
+
392
+ &__pool-grid {
393
+ display: grid;
394
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
395
+ gap: 14px;
396
+ }
397
+
398
+ &__pool-card {
399
+ border: 1px solid var(--border-color, #2a2a2a);
400
+ padding: 16px;
401
+ background: var(--surface-raised, #111);
402
+ display: flex;
403
+ flex-direction: column;
404
+ gap: 10px;
405
+
406
+ &--exhausted { opacity: 0.6; }
407
+ }
408
+
409
+ &__pool-name {
410
+ font-weight: 600;
411
+ font-size: 0.95rem;
412
+ color: var(--text-primary, #fff);
413
+ }
414
+
415
+ &__pool-availability { font-size: 0.82rem; }
416
+ &__avail-ok { color: var(--success, #22c55e); }
417
+ &__avail-empty { color: var(--text-secondary, #888); }
418
+
419
+ &__platform-sel { display: flex; flex-direction: column; gap: 6px; }
420
+
421
+ &__label {
422
+ font-size: 0.75rem;
423
+ color: var(--text-secondary, #888);
424
+ }
425
+
426
+ &__platform-chips {
427
+ display: flex;
428
+ flex-wrap: wrap;
429
+ gap: 6px;
430
+ }
431
+
432
+ &__platform-chip {
433
+ padding: 4px 10px;
434
+ font-size: 0.78rem;
435
+ border: 1px solid var(--border-color, #2a2a2a);
436
+ color: var(--text-secondary, #888);
437
+ cursor: pointer;
438
+ transition: border-color 0.15s, color 0.15s;
439
+
440
+ &--active {
441
+ border-color: var(--accent, #00d4ff);
442
+ color: var(--accent, #00d4ff);
443
+ }
444
+ }
445
+
446
+ &__description-field { display: flex; flex-direction: column; gap: 4px; }
447
+
448
+ &__textarea {
449
+ background: var(--surface, #0a0a0a);
450
+ border: 1px solid var(--border-color, #2a2a2a);
451
+ color: var(--text-primary, #fff);
452
+ font-size: 0.82rem;
453
+ padding: 8px 10px;
454
+ resize: vertical;
455
+ width: 100%;
456
+ outline: none;
457
+ font-family: inherit;
458
+
459
+ &:focus { border-color: var(--accent, #00d4ff); }
460
+ }
461
+
462
+ &__pool-footer { margin-top: auto; display: flex; align-items: center; }
463
+
464
+ &__request-btn {
465
+ padding: 8px 16px;
466
+ background: var(--accent, #00d4ff);
467
+ color: #000;
468
+ border: none;
469
+ font-weight: 600;
470
+ font-size: 0.85rem;
471
+ cursor: pointer;
472
+ width: 100%;
473
+ transition: opacity 0.15s;
474
+
475
+ &:hover { opacity: 0.85; }
476
+ &:disabled { opacity: 0.4; cursor: not-allowed; }
477
+ }
478
+
479
+ &__requested-label {
480
+ font-size: 0.82rem;
481
+ color: var(--success, #22c55e);
482
+ }
483
+
484
+ &__request-list {
485
+ display: flex;
486
+ flex-direction: column;
487
+ gap: 8px;
488
+ }
489
+
490
+ &__request-row {
491
+ display: grid;
492
+ grid-template-columns: 1fr auto auto;
493
+ align-items: center;
494
+ gap: 16px;
495
+ border: 1px solid var(--border-color, #2a2a2a);
496
+ padding: 12px 16px;
497
+ background: var(--surface-raised, #111);
498
+ }
499
+
500
+ &__request-info { display: flex; flex-direction: column; gap: 2px; }
501
+
502
+ &__request-pool {
503
+ font-weight: 500;
504
+ font-size: 0.9rem;
505
+ color: var(--text-primary, #fff);
506
+ }
507
+
508
+ &__request-platform {
509
+ font-size: 0.78rem;
510
+ color: var(--text-secondary, #888);
511
+ }
512
+
513
+ &__status-chip {
514
+ font-size: 0.73rem;
515
+ padding: 3px 10px;
516
+ border: 1px solid var(--chip-color, #888);
517
+ color: var(--chip-color, #888);
518
+ text-transform: uppercase;
519
+ letter-spacing: 0.05em;
520
+ white-space: nowrap;
521
+ }
522
+
523
+ &__request-action { display: flex; align-items: center; }
524
+
525
+ &__reveal-btn {
526
+ padding: 7px 14px;
527
+ background: var(--warning-subtle, rgba(245,158,11,0.15));
528
+ border: 1px solid var(--warning, #f59e0b);
529
+ color: var(--warning, #f59e0b);
530
+ font-weight: 600;
531
+ font-size: 0.82rem;
532
+ cursor: pointer;
533
+ transition: opacity 0.15s;
534
+
535
+ &:hover { opacity: 0.8; }
536
+ &:disabled { opacity: 0.4; cursor: not-allowed; }
537
+ }
538
+
539
+ &__revealed-label {
540
+ font-size: 0.82rem;
541
+ color: var(--success, #22c55e);
542
+ }
543
+
544
+ &__pending-label {
545
+ font-size: 0.8rem;
546
+ color: var(--text-secondary, #888);
547
+ font-style: italic;
548
+ }
549
+ }
550
+ </style>
@@ -0,0 +1,142 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * One-time key reveal modal.
4
+ * Shows a warning that the code will be displayed once, then shows it.
5
+ */
6
+ const props = defineProps<{
7
+ code: string
8
+ platform: string | null
9
+ revealedAt: string
10
+ }>()
11
+
12
+ const emit = defineEmits<{ (e: 'close'): void }>()
13
+
14
+ function copyCode() {
15
+ navigator.clipboard?.writeText(props.code)
16
+ }
17
+ </script>
18
+
19
+ <template>
20
+ <div class="krm__overlay" @click.self="emit('close')">
21
+ <div class="krm__panel">
22
+ <div class="krm__icon">🔑</div>
23
+ <h2 class="krm__title">{{ $t('keys.reveal.title') }}</h2>
24
+ <p class="krm__sub">{{ $t('keys.reveal.sub') }}</p>
25
+
26
+ <div v-if="platform" class="krm__platform">{{ platform }}</div>
27
+
28
+ <div class="krm__code-box">
29
+ <span class="krm__code">{{ code }}</span>
30
+ <button class="krm__copy" @click="copyCode" :title="$t('common.copy')">
31
+ &#x2398;
32
+ </button>
33
+ </div>
34
+
35
+ <p class="krm__warning">{{ $t('keys.reveal.warning') }}</p>
36
+
37
+ <button class="krm__close-btn" @click="emit('close')">
38
+ {{ $t('common.close') }}
39
+ </button>
40
+ </div>
41
+ </div>
42
+ </template>
43
+
44
+ <style scoped lang="scss">
45
+ .krm {
46
+ &__overlay {
47
+ position: fixed;
48
+ inset: 0;
49
+ background: rgba(0, 0, 0, 0.75);
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ z-index: 1000;
54
+ }
55
+
56
+ &__panel {
57
+ background: var(--surface, #141414);
58
+ border: 1px solid var(--border-color, #2a2a2a);
59
+ padding: 40px 32px;
60
+ max-width: 440px;
61
+ width: 100%;
62
+ text-align: center;
63
+ display: flex;
64
+ flex-direction: column;
65
+ align-items: center;
66
+ gap: 14px;
67
+ }
68
+
69
+ &__icon { font-size: 2rem; }
70
+
71
+ &__title {
72
+ font-size: 1.2rem;
73
+ font-weight: 700;
74
+ color: var(--text-primary, #fff);
75
+ margin: 0;
76
+ }
77
+
78
+ &__sub {
79
+ font-size: 0.88rem;
80
+ color: var(--text-secondary, #888);
81
+ margin: 0;
82
+ }
83
+
84
+ &__platform {
85
+ font-size: 0.8rem;
86
+ padding: 3px 12px;
87
+ border: 1px solid var(--accent, #00d4ff);
88
+ color: var(--accent, #00d4ff);
89
+ text-transform: uppercase;
90
+ letter-spacing: 0.08em;
91
+ }
92
+
93
+ &__code-box {
94
+ display: flex;
95
+ align-items: center;
96
+ gap: 10px;
97
+ background: var(--surface-raised, #1a1a1a);
98
+ border: 1px solid var(--border-color, #2a2a2a);
99
+ padding: 14px 20px;
100
+ width: 100%;
101
+ justify-content: space-between;
102
+ }
103
+
104
+ &__code {
105
+ font-family: monospace;
106
+ font-size: 1.05rem;
107
+ color: var(--success, #22c55e);
108
+ letter-spacing: 0.1em;
109
+ word-break: break-all;
110
+ }
111
+
112
+ &__copy {
113
+ background: none;
114
+ border: none;
115
+ color: var(--text-secondary, #888);
116
+ cursor: pointer;
117
+ font-size: 1.1rem;
118
+ padding: 4px;
119
+ transition: color 0.15s;
120
+ flex-shrink: 0;
121
+ &:hover { color: var(--text-primary, #fff); }
122
+ }
123
+
124
+ &__warning {
125
+ font-size: 0.78rem;
126
+ color: var(--warning, #f59e0b);
127
+ margin: 0;
128
+ }
129
+
130
+ &__close-btn {
131
+ margin-top: 6px;
132
+ padding: 10px 28px;
133
+ background: var(--surface-raised, #1e1e1e);
134
+ border: 1px solid var(--border-color, #2a2a2a);
135
+ color: var(--text-primary, #fff);
136
+ cursor: pointer;
137
+ font-size: 0.9rem;
138
+ transition: background 0.15s;
139
+ &:hover { background: var(--surface-hover, #2a2a2a); }
140
+ }
141
+ }
142
+ </style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.1.44",
3
+ "version": "1.1.46",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",