@mundogamernetwork/shared-ui 1.3.7 → 1.4.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/form/AppSelect.vue +111 -0
- package/components/form/SortInput.vue +98 -0
- package/components/indie-wall/MediaKitWallBlock.vue +408 -0
- package/components/indie-wall/StepCustomize.vue +8 -0
- package/components/indie-wall/StepGoal.vue +11 -1
- package/components/indie-wall/StepPackage.vue +13 -3
- package/components/indie-wall/StepPay.vue +11 -1
- package/components/indie-wall/SupportStepper.vue +9 -1
- package/components/ui/MgHeaderUIUser.vue +10 -1
- package/locales/de.json +2 -1
- package/locales/en.json +2 -1
- package/locales/pt-BR.json +2 -1
- package/locales/ro.json +2 -1
- package/package.json +1 -1
- package/pages/key-campaigns/[slug].vue +22 -4
- package/pages/key-campaigns/index.vue +69 -82
- package/pages/media-kit/[slug].vue +44 -53
- package/pages/mural/[slug].vue +216 -14
- package/pages/presskit/game/[slug].vue +29 -18
- package/public/imgs/sort-active.png +0 -0
- package/services/campaignService.ts +4 -0
package/pages/mural/[slug].vue
CHANGED
|
@@ -4,7 +4,7 @@ import SupportStepper from '../../components/indie-wall/SupportStepper.vue'
|
|
|
4
4
|
import MuralCanvas from '../../components/indie-wall/MuralCanvas.vue'
|
|
5
5
|
import IndieWallLeaderboard from '../../components/indie-wall/IndieWallLeaderboard.vue'
|
|
6
6
|
|
|
7
|
-
const { t } = useI18n()
|
|
7
|
+
const { t, locale } = useI18n()
|
|
8
8
|
const route = useRoute()
|
|
9
9
|
const router = useRouter()
|
|
10
10
|
const wallSlug = route.params.slug as string
|
|
@@ -142,6 +142,15 @@ const goals = ref<any[]>([])
|
|
|
142
142
|
const supporters = ref<any[]>([])
|
|
143
143
|
const loading = ref(true)
|
|
144
144
|
|
|
145
|
+
// SSR-safe fetch so bots/link-preview scrapers (which don't run JS) get the
|
|
146
|
+
// real wall data for the SEO tags below, without waiting for the client-side
|
|
147
|
+
// onMounted → loadWall() refresh (which still runs for goals/supporters/WS).
|
|
148
|
+
const { data: ssrWallData } = await useAsyncData(
|
|
149
|
+
`mural-wall-${wallSlug}`,
|
|
150
|
+
() => fetchPublicWall(wallSlug).then(r => r.data?.data || r.data).catch(() => null),
|
|
151
|
+
)
|
|
152
|
+
if (ssrWallData.value) wall.value = ssrWallData.value
|
|
153
|
+
|
|
145
154
|
// ── Stepper state ──────────────────────────────────────────────────────────
|
|
146
155
|
const showStepper = ref(false)
|
|
147
156
|
const stepperStartStep = ref<1 | 2 | 3 | 4 | 5>(1)
|
|
@@ -163,6 +172,15 @@ const progressPercent = computed(() => {
|
|
|
163
172
|
return total > 0 ? Math.min(100, ((wall.value.used_pixels || 0) / total) * 100) : 0
|
|
164
173
|
})
|
|
165
174
|
|
|
175
|
+
// Locale-aware currency symbol + consistent decimals (was raw "BRL 500" string concat).
|
|
176
|
+
function formatMoney(amount: number, currency?: string | null) {
|
|
177
|
+
try {
|
|
178
|
+
return new Intl.NumberFormat(locale.value, { style: 'currency', currency: currency || 'USD' }).format(amount)
|
|
179
|
+
} catch {
|
|
180
|
+
return `${currency || ''} ${Number(amount).toFixed(2)}`.trim()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
166
184
|
// ── Load ────────────────────────────────────────────────────────────────────
|
|
167
185
|
async function loadWall() {
|
|
168
186
|
loading.value = true
|
|
@@ -191,6 +209,13 @@ async function loadWall() {
|
|
|
191
209
|
stepperStartStep.value = 1
|
|
192
210
|
showStepper.value = true
|
|
193
211
|
}
|
|
212
|
+
|
|
213
|
+
// Deep-link from a supporter's own share link (?pixel=123): open their
|
|
214
|
+
// pixel detail automatically so the shared link lands on their spot.
|
|
215
|
+
if (route.query.pixel) {
|
|
216
|
+
const shared = supporters.value.find((s: any) => String(s.id) === String(route.query.pixel))
|
|
217
|
+
if (shared) detailPixel.value = shared
|
|
218
|
+
}
|
|
194
219
|
} catch (err) {
|
|
195
220
|
console.error(err)
|
|
196
221
|
} finally {
|
|
@@ -214,8 +239,19 @@ function onCanvasTapEmpty(coord: { x: number; y: number }) {
|
|
|
214
239
|
})
|
|
215
240
|
}
|
|
216
241
|
|
|
217
|
-
function
|
|
242
|
+
function openPixelDetail(s: any) {
|
|
218
243
|
detailPixel.value = s
|
|
244
|
+
router.replace({ query: { ...route.query, pixel: s.id } })
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function closePixelDetail() {
|
|
248
|
+
detailPixel.value = null
|
|
249
|
+
const { pixel: _omit, ...rest } = route.query
|
|
250
|
+
router.replace({ query: rest })
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function onCanvasTapSupporter(s: any) {
|
|
254
|
+
openPixelDetail(s)
|
|
219
255
|
}
|
|
220
256
|
|
|
221
257
|
const hoverTip = ref<{ supporter: any; x: number; y: number } | null>(null)
|
|
@@ -248,10 +284,93 @@ function supporterDisplayName(s: any): string {
|
|
|
248
284
|
return s.title || s.user?.name || t('tv.dashboard.indie_wall.anonymous')
|
|
249
285
|
}
|
|
250
286
|
|
|
251
|
-
|
|
252
|
-
|
|
287
|
+
const requestUrl = useRequestURL()
|
|
288
|
+
|
|
289
|
+
// When a supporter's own pixel is open (either they just clicked it, or a
|
|
290
|
+
// visitor followed that supporter's share link), the page's own title/
|
|
291
|
+
// description swap to their name+message so *their* share card, not just
|
|
292
|
+
// the wall's, reads correctly. Image stays the wall's own cover — pixel
|
|
293
|
+
// images are inline base64, not real URLs, so crawlers can't fetch them.
|
|
294
|
+
const seoTitle = computed(() => {
|
|
295
|
+
if (detailPixel.value && wall.value) {
|
|
296
|
+
return t('tv.dashboard.indie_wall.share_pixel_title', { name: supporterDisplayName(detailPixel.value), wall: wall.value.name })
|
|
297
|
+
}
|
|
298
|
+
return wall.value?.name ? `${wall.value.name} · Mundo Gamer Wall` : 'Mundo Gamer Wall'
|
|
299
|
+
})
|
|
300
|
+
const seoDescription = computed(() => {
|
|
301
|
+
if (detailPixel.value) {
|
|
302
|
+
return detailPixel.value.supporter_message
|
|
303
|
+
|| (wall.value?.name ? t('tv.dashboard.indie_wall.share_pixel_default_desc', { wall: wall.value.name }) : '')
|
|
304
|
+
}
|
|
305
|
+
return wall.value?.description || wall.value?.long_description?.replace(/\n+/g, ' ').slice(0, 160) || ''
|
|
306
|
+
})
|
|
307
|
+
const seoImage = computed(() => wall.value?.cover_image_url || wall.value?.logo_url || '')
|
|
308
|
+
|
|
309
|
+
// ── Per-supporter share link ────────────────────────────────────────────────
|
|
310
|
+
const sharePixelUrl = computed(() => {
|
|
311
|
+
if (!detailPixel.value) return requestUrl.href
|
|
312
|
+
const base = `${requestUrl.origin}${route.path}`
|
|
313
|
+
return `${base}?pixel=${detailPixel.value.id}`
|
|
314
|
+
})
|
|
315
|
+
const shareText = computed(() => {
|
|
316
|
+
if (!detailPixel.value || !wall.value) return ''
|
|
317
|
+
return t('tv.dashboard.indie_wall.share_pixel_text', { name: supporterDisplayName(detailPixel.value), wall: wall.value.name })
|
|
318
|
+
})
|
|
319
|
+
const shareTwitterUrl = computed(() => `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText.value)}&url=${encodeURIComponent(sharePixelUrl.value)}`)
|
|
320
|
+
const shareWhatsappUrl = computed(() => `https://wa.me/?text=${encodeURIComponent(`${shareText.value} ${sharePixelUrl.value}`)}`)
|
|
321
|
+
const shareFacebookUrl = computed(() => `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(sharePixelUrl.value)}`)
|
|
322
|
+
|
|
323
|
+
const linkCopied = ref(false)
|
|
324
|
+
async function copyShareLink() {
|
|
325
|
+
try {
|
|
326
|
+
await navigator.clipboard.writeText(sharePixelUrl.value)
|
|
327
|
+
linkCopied.value = true
|
|
328
|
+
setTimeout(() => { linkCopied.value = false }, 2000)
|
|
329
|
+
} catch { /* clipboard unavailable */ }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// "Create your own wall" footer CTA — same host map as the backend's
|
|
333
|
+
// IndieWallController::resolverResponse, so it always points at whichever
|
|
334
|
+
// platform (TV/Agency) actually owns this wall rather than a hardcoded host.
|
|
335
|
+
const CREATE_WALL_HOST_MAP: Record<string, string> = {
|
|
336
|
+
MGTV: 'https://mundogamer.tv',
|
|
337
|
+
MGAG: 'https://mundogamer.agency',
|
|
338
|
+
}
|
|
339
|
+
const createWallUrl = computed(() => {
|
|
340
|
+
const shortName = wall.value?.origin_system?.short_name
|
|
341
|
+
const host = (shortName && CREATE_WALL_HOST_MAP[shortName]) || CREATE_WALL_HOST_MAP.MGTV
|
|
342
|
+
return `${host}/${locale.value}/dashboard/wall`
|
|
253
343
|
})
|
|
254
344
|
|
|
345
|
+
// Raw useHead meta-array form — matches the exact mechanism the site-wide
|
|
346
|
+
// defaults in nuxt.config.ts use, and is proven to reliably override by
|
|
347
|
+
// name/property key in this app. useSeoMeta was tried first but its og:*/
|
|
348
|
+
// twitter:*/description overrides were silently dropped here (stray
|
|
349
|
+
// duplicate `unhead`/`@vueuse/head` deps in community-frontend/package.json
|
|
350
|
+
// likely cause a resolution conflict) — only `title` and `ogUrl` came
|
|
351
|
+
// through. Do not switch this back to useSeoMeta without first removing
|
|
352
|
+
// those stray deps and re-verifying against raw SSR HTML (curl), not just
|
|
353
|
+
// the browser, since hydration can mask a server-side-only failure.
|
|
354
|
+
useHead(() => ({
|
|
355
|
+
title: seoTitle.value,
|
|
356
|
+
link: [{ rel: 'canonical', href: requestUrl.href }],
|
|
357
|
+
meta: [
|
|
358
|
+
{ name: 'description', content: seoDescription.value },
|
|
359
|
+
{ property: 'og:title', content: seoTitle.value },
|
|
360
|
+
{ property: 'og:description', content: seoDescription.value },
|
|
361
|
+
// Only override the image when the wall actually has one — an empty
|
|
362
|
+
// string would replace (not fall back to) the site default image.
|
|
363
|
+
...(seoImage.value ? [{ property: 'og:image', content: seoImage.value }] : []),
|
|
364
|
+
{ property: 'og:type', content: 'website' },
|
|
365
|
+
{ property: 'og:url', content: requestUrl.href },
|
|
366
|
+
{ property: 'og:site_name', content: 'Mundo Gamer Wall' },
|
|
367
|
+
{ name: 'twitter:card', content: 'summary_large_image' },
|
|
368
|
+
{ name: 'twitter:title', content: seoTitle.value },
|
|
369
|
+
{ name: 'twitter:description', content: seoDescription.value },
|
|
370
|
+
...(seoImage.value ? [{ name: 'twitter:image', content: seoImage.value }] : []),
|
|
371
|
+
],
|
|
372
|
+
}))
|
|
373
|
+
|
|
255
374
|
onMounted(async () => {
|
|
256
375
|
await loadWall()
|
|
257
376
|
if (wall.value?.id) subscribeWall(wall.value.id)
|
|
@@ -318,11 +437,15 @@ onUnmounted(() => {
|
|
|
318
437
|
<div class="hero-overlay">
|
|
319
438
|
<img v-if="wall.logo_url" :src="wall.logo_url" class="hero-logo" alt="" />
|
|
320
439
|
<h1 class="hero-title">{{ wall.name }}</h1>
|
|
440
|
+
<div v-if="wall.owner_nickname || wall.owner_name" class="hero-owner">
|
|
441
|
+
<img v-if="wall.owner_avatar_url" :src="wall.owner_avatar_url" class="hero-owner-avatar" alt="" />
|
|
442
|
+
<span class="hero-owner-name">{{ $t('tv.dashboard.indie_wall.mural_by', { name: wall.owner_nickname || wall.owner_name }) }}</span>
|
|
443
|
+
</div>
|
|
321
444
|
<p v-if="wall.description" class="hero-desc">{{ wall.description }}</p>
|
|
322
445
|
|
|
323
446
|
<div class="hero-stats">
|
|
324
447
|
<div class="hero-stat">
|
|
325
|
-
<span class="hero-stat-value">{{
|
|
448
|
+
<span class="hero-stat-value">{{ formatMoney(Number(wall.raised_amount || 0), wall.currency) }}</span>
|
|
326
449
|
<span class="hero-stat-label">{{ $t('tv.dashboard.indie_wall.raised') }}</span>
|
|
327
450
|
</div>
|
|
328
451
|
<div class="hero-stat-divider" />
|
|
@@ -372,7 +495,7 @@ onUnmounted(() => {
|
|
|
372
495
|
{{ $t('tv.dashboard.indie_wall.reached') }}
|
|
373
496
|
</span>
|
|
374
497
|
<span v-else-if="goal.target_amount > 0" class="goal-amount">
|
|
375
|
-
{{
|
|
498
|
+
{{ formatMoney(Number(goal.target_amount), wall.currency) }}
|
|
376
499
|
</span>
|
|
377
500
|
<span v-else class="goal-amount">
|
|
378
501
|
{{ goal.target_pixels }} {{ $t('tv.dashboard.indie_wall.spots_unit') }}
|
|
@@ -396,7 +519,7 @@ onUnmounted(() => {
|
|
|
396
519
|
<div class="goal-progress-fill" :style="{ width: (goal.progress_percent || 0) + '%' }" />
|
|
397
520
|
<span class="goal-progress-text">
|
|
398
521
|
<template v-if="goal.target_amount > 0">
|
|
399
|
-
{{ $t('tv.dashboard.indie_wall.goal_card_missing_amount', {
|
|
522
|
+
{{ $t('tv.dashboard.indie_wall.goal_card_missing_amount', { amount: formatMoney(Math.max(0, goal.target_amount - (goal.current_amount || 0)), wall.currency) }) }}
|
|
400
523
|
</template>
|
|
401
524
|
<template v-else>
|
|
402
525
|
{{ $t('tv.dashboard.indie_wall.goal_card_missing', { count: Math.max(0, (goal.target_pixels || 0) - (goal.current_pixels || 0)) }) }}
|
|
@@ -433,7 +556,7 @@ onUnmounted(() => {
|
|
|
433
556
|
>
|
|
434
557
|
<img v-if="tier.image_url" :src="tier.image_url" class="tier-img" alt="" />
|
|
435
558
|
<h3 class="tier-name">{{ tier.name }}</h3>
|
|
436
|
-
<div class="tier-price">{{ tier.currency || wall.currency
|
|
559
|
+
<div class="tier-price">{{ formatMoney(Number(tier.price), tier.currency || wall.currency) }}</div>
|
|
437
560
|
<p class="tier-pixels">{{ tier.pixel_count }} {{ $t('tv.dashboard.indie_wall.spots_unit') }}</p>
|
|
438
561
|
<p v-if="tier.description" class="tier-desc">{{ tier.description }}</p>
|
|
439
562
|
<div v-if="tier.max_quantity" class="tier-stock">
|
|
@@ -471,7 +594,7 @@ onUnmounted(() => {
|
|
|
471
594
|
<section v-if="supporters.length" class="section">
|
|
472
595
|
<h2 class="section-title">{{ $t('tv.dashboard.indie_wall.recent_supporters') }}</h2>
|
|
473
596
|
<div class="supporters-grid">
|
|
474
|
-
<div v-for="s in supporters.slice(0, 24)" :key="s.id" class="supp-card" @click="
|
|
597
|
+
<div v-for="s in supporters.slice(0, 24)" :key="s.id" class="supp-card" @click="openPixelDetail(s)">
|
|
475
598
|
<div class="supp-pixel">
|
|
476
599
|
<img v-if="s.image_url" :src="s.image_url" class="supp-pixel-img" alt="" />
|
|
477
600
|
<div v-else class="supp-pixel-swatch" :style="{ background: s.background_color || '#272930' }" />
|
|
@@ -484,7 +607,7 @@ onUnmounted(() => {
|
|
|
484
607
|
<span class="supp-name">{{ supporterDisplayName(s) }}</span>
|
|
485
608
|
<span v-if="!s.is_anonymous && !s.user && s.guest_email" class="supp-email">{{ s.guest_email }}</span>
|
|
486
609
|
</div>
|
|
487
|
-
<span class="supp-amount">{{
|
|
610
|
+
<span class="supp-amount">{{ formatMoney(Number(s.amount_paid || 0), wall.currency) }}</span>
|
|
488
611
|
</div>
|
|
489
612
|
<p v-if="s.message || s.supporter_message" class="supp-msg">"{{ s.message || s.supporter_message }}"</p>
|
|
490
613
|
</div>
|
|
@@ -497,6 +620,16 @@ onUnmounted(() => {
|
|
|
497
620
|
<IndieWallLeaderboard :supporters="supporters" />
|
|
498
621
|
</section>
|
|
499
622
|
|
|
623
|
+
<!-- ── Footer credit ─────────────────────────────────────────────────── -->
|
|
624
|
+
<footer class="mural-footer">
|
|
625
|
+
<p class="mural-footer-text">
|
|
626
|
+
{{ $t('tv.dashboard.indie_wall.footer_made_with') }} <strong>Mundo Gamer Wall</strong>
|
|
627
|
+
</p>
|
|
628
|
+
<a :href="createWallUrl" target="_blank" rel="noopener" class="mural-footer-cta">
|
|
629
|
+
{{ $t('tv.dashboard.indie_wall.footer_create_cta') }}
|
|
630
|
+
</a>
|
|
631
|
+
</footer>
|
|
632
|
+
|
|
500
633
|
</div>
|
|
501
634
|
|
|
502
635
|
<!-- ── Overlays ────────────────────────────────────────────────────────── -->
|
|
@@ -568,7 +701,7 @@ onUnmounted(() => {
|
|
|
568
701
|
<p class="hover-tip-name">{{ supporterDisplayName(hoverTip.supporter) }}</p>
|
|
569
702
|
<p v-if="hoverTip.supporter.supporter_message" class="hover-tip-msg">"{{ hoverTip.supporter.supporter_message }}"</p>
|
|
570
703
|
<div class="hover-tip-badges">
|
|
571
|
-
<span class="hover-tip-amount">{{
|
|
704
|
+
<span class="hover-tip-amount">{{ formatMoney(Number(hoverTip.supporter.amount_paid || 0), wall.currency) }}</span>
|
|
572
705
|
<span class="hover-tip-pixels">{{ hoverTip.supporter.pixel_count }} {{ $t('tv.dashboard.indie_wall.spots_unit') }}</span>
|
|
573
706
|
</div>
|
|
574
707
|
</div>
|
|
@@ -576,9 +709,9 @@ onUnmounted(() => {
|
|
|
576
709
|
</Transition>
|
|
577
710
|
|
|
578
711
|
<Transition name="fade">
|
|
579
|
-
<div v-if="detailPixel" class="detail-overlay" @click.self="
|
|
712
|
+
<div v-if="detailPixel" class="detail-overlay" @click.self="closePixelDetail">
|
|
580
713
|
<div class="detail-box">
|
|
581
|
-
<button class="overlay-close" type="button" @click="
|
|
714
|
+
<button class="overlay-close" type="button" @click="closePixelDetail">×</button>
|
|
582
715
|
<div
|
|
583
716
|
class="detail-hero"
|
|
584
717
|
:style="{ background: detailPixel.background_color || '#272930' }"
|
|
@@ -592,7 +725,7 @@ onUnmounted(() => {
|
|
|
592
725
|
<div class="detail-amount-row">
|
|
593
726
|
<span class="detail-amount-label">{{ $t('tv.dashboard.indie_wall.spots_unit') }}</span>
|
|
594
727
|
<span class="detail-amount-value">
|
|
595
|
-
|
|
728
|
+
{{ formatMoney(Number(detailPixel.amount_paid), wall?.currency) }}
|
|
596
729
|
</span>
|
|
597
730
|
</div>
|
|
598
731
|
<div class="detail-chips">
|
|
@@ -602,6 +735,24 @@ onUnmounted(() => {
|
|
|
602
735
|
<a v-if="detailPixel.link" :href="detailPixel.link" target="_blank" rel="noopener" class="detail-link">
|
|
603
736
|
{{ detailPixel.link }}
|
|
604
737
|
</a>
|
|
738
|
+
|
|
739
|
+
<!-- ── Share this pixel ─────────────────────────────────────── -->
|
|
740
|
+
<div class="detail-share">
|
|
741
|
+
<p class="detail-share-title">{{ $t('tv.dashboard.indie_wall.share_pixel_cta') }}</p>
|
|
742
|
+
<div class="detail-share-row">
|
|
743
|
+
<a :href="shareTwitterUrl" target="_blank" rel="noopener" class="detail-share-btn" :aria-label="$t('tv.dashboard.indie_wall.share_via_twitter')">𝕏</a>
|
|
744
|
+
<a :href="shareWhatsappUrl" target="_blank" rel="noopener" class="detail-share-btn" :aria-label="$t('tv.dashboard.indie_wall.share_via_whatsapp')">
|
|
745
|
+
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><path d="M12.04 2C6.58 2 2.13 6.45 2.13 11.91c0 1.75.46 3.45 1.32 4.95L2.05 22l5.25-1.38a9.9 9.9 0 0 0 4.74 1.2h.01c5.46 0 9.9-4.45 9.9-9.91 0-2.65-1.03-5.14-2.9-7.01A9.82 9.82 0 0 0 12.04 2Z" opacity="0"/><path d="M9.2 6.8c-.2-.45-.42-.46-.62-.47h-.53c-.18 0-.48.07-.73.34-.25.27-.96.93-.96 2.28s.98 2.65 1.12 2.83c.14.18 1.9 3.03 4.7 4.14 2.32.92 2.8.74 3.3.7.5-.05 1.62-.66 1.85-1.3.23-.63.23-1.17.16-1.29-.07-.11-.25-.18-.53-.32-.27-.14-1.62-.8-1.87-.89-.25-.09-.43-.14-.62.14-.18.27-.7.89-.86 1.07-.16.18-.32.2-.59.07-.27-.14-1.15-.42-2.19-1.35-.81-.72-1.36-1.62-1.52-1.89-.16-.27-.02-.42.12-.55.13-.13.27-.33.4-.5.13-.16.18-.27.27-.45.09-.18.05-.34-.02-.48-.07-.14-.6-1.5-.85-2.05Z"/></svg>
|
|
746
|
+
</a>
|
|
747
|
+
<a :href="shareFacebookUrl" target="_blank" rel="noopener" class="detail-share-btn" :aria-label="$t('tv.dashboard.indie_wall.share_via_facebook')">f</a>
|
|
748
|
+
<button type="button" class="detail-share-btn" @click="copyShareLink" :aria-label="$t('tv.dashboard.indie_wall.share_copy_link')">
|
|
749
|
+
{{ linkCopied ? '✓' : '🔗' }}
|
|
750
|
+
</button>
|
|
751
|
+
</div>
|
|
752
|
+
<button type="button" class="detail-share-cta" @click="closePixelDetail(); openStepper({ startStep: 1 })">
|
|
753
|
+
{{ $t('tv.dashboard.indie_wall.share_join_cta') }}
|
|
754
|
+
</button>
|
|
755
|
+
</div>
|
|
605
756
|
</div>
|
|
606
757
|
</div>
|
|
607
758
|
</div>
|
|
@@ -647,6 +798,9 @@ onUnmounted(() => {
|
|
|
647
798
|
margin-bottom: 16px; border: 2px solid rgba(255,255,255,0.1);
|
|
648
799
|
}
|
|
649
800
|
.hero-title { font-size: 2rem; font-weight: 800; color: var(--title-fg, #fff); margin: 0 0 12px; letter-spacing: -0.5px; }
|
|
801
|
+
.hero-owner { display: flex; align-items: center; gap: 8px; margin: -6px 0 12px; }
|
|
802
|
+
.hero-owner-avatar { width: 24px; height: 24px; border-radius: 50%; object-fit: cover; }
|
|
803
|
+
.hero-owner-name { color: rgba(255,255,255,0.75); font-size: 0.9rem; font-weight: 500; }
|
|
650
804
|
.hero-desc { color: rgba(255,255,255,0.65); font-size: 1rem; max-width: 600px; line-height: 1.6; margin: 0 0 24px; }
|
|
651
805
|
.hero-stats { display: flex; align-items: center; gap: 24px; margin-bottom: 16px; }
|
|
652
806
|
.hero-stat { display: flex; flex-direction: column; align-items: center; gap: 2px; }
|
|
@@ -930,6 +1084,32 @@ onUnmounted(() => {
|
|
|
930
1084
|
&:hover { text-decoration: underline; }
|
|
931
1085
|
}
|
|
932
1086
|
|
|
1087
|
+
.detail-share {
|
|
1088
|
+
margin-top: 16px; padding-top: 16px;
|
|
1089
|
+
border-top: 1px solid #272930;
|
|
1090
|
+
}
|
|
1091
|
+
.detail-share-title {
|
|
1092
|
+
margin: 0 0 8px; font-size: 0.78rem; color: var(--secondary-info-fg, #aaa);
|
|
1093
|
+
}
|
|
1094
|
+
.detail-share-row {
|
|
1095
|
+
display: flex; align-items: center; gap: 8px; margin-bottom: 10px;
|
|
1096
|
+
}
|
|
1097
|
+
.detail-share-btn {
|
|
1098
|
+
width: 34px; height: 34px; flex-shrink: 0;
|
|
1099
|
+
display: flex; align-items: center; justify-content: center;
|
|
1100
|
+
background: rgba(255,255,255,0.06); border: 1px solid #272930;
|
|
1101
|
+
color: var(--title-fg, #fff); font-size: 0.9rem; font-weight: 700;
|
|
1102
|
+
cursor: pointer; text-decoration: none; transition: border-color 0.15s;
|
|
1103
|
+
&:hover { border-color: var(--chip-text, #D297FF); }
|
|
1104
|
+
}
|
|
1105
|
+
.detail-share-cta {
|
|
1106
|
+
width: 100%;
|
|
1107
|
+
background: var(--chip-text, #D297FF); color: #13161C;
|
|
1108
|
+
border: none; padding: 10px 14px; font-weight: 700; font-size: 0.85rem;
|
|
1109
|
+
cursor: pointer; transition: opacity 0.15s;
|
|
1110
|
+
&:hover { opacity: 0.88; }
|
|
1111
|
+
}
|
|
1112
|
+
|
|
933
1113
|
.fade-enter-active, .fade-leave-active { transition: opacity 0.18s; }
|
|
934
1114
|
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
|
935
1115
|
|
|
@@ -1047,6 +1227,28 @@ onUnmounted(() => {
|
|
|
1047
1227
|
background: rgba(255,255,255,0.06); padding: 2px 6px;
|
|
1048
1228
|
}
|
|
1049
1229
|
|
|
1230
|
+
// ── Footer credit ─────────────────────────────────────────────────────────
|
|
1231
|
+
.mural-footer {
|
|
1232
|
+
display: flex; flex-direction: column; align-items: center; gap: 12px;
|
|
1233
|
+
padding: 48px 16px; margin-top: 8px;
|
|
1234
|
+
border-top: 1px solid #1e2028;
|
|
1235
|
+
text-align: center;
|
|
1236
|
+
}
|
|
1237
|
+
.mural-footer-text {
|
|
1238
|
+
font-size: 0.8rem; color: var(--secondary-info-fg, #888); margin: 0;
|
|
1239
|
+
strong { color: var(--title-fg, #fff); }
|
|
1240
|
+
}
|
|
1241
|
+
.mural-footer-cta {
|
|
1242
|
+
display: inline-block;
|
|
1243
|
+
padding: 8px 20px;
|
|
1244
|
+
border: 1px solid var(--chip-text, #D297FF);
|
|
1245
|
+
color: var(--chip-text, #D297FF);
|
|
1246
|
+
font-size: 0.8rem; font-weight: 600;
|
|
1247
|
+
text-decoration: none;
|
|
1248
|
+
transition: background 0.15s, color 0.15s;
|
|
1249
|
+
&:hover { background: var(--chip-text, #D297FF); color: #13161C; }
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1050
1252
|
@media (max-width: 640px) {
|
|
1051
1253
|
.hero-title { font-size: 1.5rem; }
|
|
1052
1254
|
.hero-stats { gap: 12px; }
|
|
@@ -86,6 +86,7 @@ const layout = computed(() => kit.value?.layout || 'classic');
|
|
|
86
86
|
const allowDownload = computed(() => kit.value?.allow_asset_download !== false);
|
|
87
87
|
const hasDownloadableAssets = computed(() => assets.value.length > 0);
|
|
88
88
|
const canDownload = computed(() => allowDownload.value && hasDownloadableAssets.value);
|
|
89
|
+
const zipDownloadUrl = computed(() => `${apiBase}/public/press-kits/${slug}/download${previewToken ? `?preview_token=${previewToken}` : ''}`);
|
|
89
90
|
|
|
90
91
|
const keyPoolsVisible = ref(false);
|
|
91
92
|
function onPoolsReady(count: number) {
|
|
@@ -179,27 +180,37 @@ function track(type: 'click' | 'download' | 'contact') {
|
|
|
179
180
|
|
|
180
181
|
const requestUrl = useRequestURL();
|
|
181
182
|
|
|
182
|
-
const seoTitle = computed(() => kit.value ? `${kit.value.name} — Press Kit` : 'Press Kit');
|
|
183
|
+
const seoTitle = computed(() => kit.value ? `${kit.value.name} — Mundo Gamer Press Kit` : 'Mundo Gamer Press Kit');
|
|
183
184
|
const seoDescription = computed(() => kit.value?.tagline || kit.value?.description?.replace(/<[^>]*>/g, '').slice(0, 160) || '');
|
|
184
185
|
const seoImage = computed(() => kit.value?.hero_image_url || kit.value?.cover_image_url || kit.value?.logo_url || '');
|
|
185
186
|
|
|
187
|
+
// Raw useHead meta-array form, not useSeoMeta — see the matching comment in
|
|
188
|
+
// shared-ui/pages/mural/[slug].vue for why (useSeoMeta's og:*/twitter:*/
|
|
189
|
+
// description overrides get silently dropped in this app).
|
|
186
190
|
useHead(() => ({
|
|
187
191
|
title: seoTitle.value,
|
|
192
|
+
link: [{ rel: 'canonical', href: requestUrl.href }],
|
|
193
|
+
// Studio's own GA4 tag, opt-in per kit — free feature, no plan gate.
|
|
194
|
+
script: kit.value?.analytics_ga_id ? [
|
|
195
|
+
{ src: `https://www.googletagmanager.com/gtag/js?id=${kit.value.analytics_ga_id}`, async: true },
|
|
196
|
+
{ innerHTML: `window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','${kit.value.analytics_ga_id}');` },
|
|
197
|
+
] : [],
|
|
198
|
+
meta: [
|
|
199
|
+
{ name: 'description', content: seoDescription.value },
|
|
200
|
+
{ property: 'og:title', content: seoTitle.value },
|
|
201
|
+
{ property: 'og:description', content: seoDescription.value },
|
|
202
|
+
// Only override the image when the kit actually has one — an empty
|
|
203
|
+
// string would replace (not fall back to) the site default image.
|
|
204
|
+
...(seoImage.value ? [{ property: 'og:image', content: seoImage.value }] : []),
|
|
205
|
+
{ property: 'og:type', content: 'article' },
|
|
206
|
+
{ property: 'og:url', content: requestUrl.href },
|
|
207
|
+
{ property: 'og:site_name', content: 'Mundo Gamer Press Kit' },
|
|
208
|
+
{ name: 'twitter:card', content: 'summary_large_image' },
|
|
209
|
+
{ name: 'twitter:title', content: seoTitle.value },
|
|
210
|
+
{ name: 'twitter:description', content: seoDescription.value },
|
|
211
|
+
...(seoImage.value ? [{ name: 'twitter:image', content: seoImage.value }] : []),
|
|
212
|
+
],
|
|
188
213
|
}));
|
|
189
|
-
|
|
190
|
-
useSeoMeta({
|
|
191
|
-
title: () => seoTitle.value,
|
|
192
|
-
description: () => seoDescription.value,
|
|
193
|
-
ogTitle: () => seoTitle.value,
|
|
194
|
-
ogDescription: () => seoDescription.value,
|
|
195
|
-
ogImage: () => seoImage.value,
|
|
196
|
-
ogType: 'article',
|
|
197
|
-
ogUrl: () => requestUrl.href,
|
|
198
|
-
twitterCard: 'summary_large_image',
|
|
199
|
-
twitterTitle: () => seoTitle.value,
|
|
200
|
-
twitterDescription: () => seoDescription.value,
|
|
201
|
-
twitterImage: () => seoImage.value,
|
|
202
|
-
});
|
|
203
214
|
</script>
|
|
204
215
|
|
|
205
216
|
<template>
|
|
@@ -247,7 +258,7 @@ useSeoMeta({
|
|
|
247
258
|
<p v-if="kit.developer" class="kit-kicker">{{ kit.developer }}</p>
|
|
248
259
|
<div class="kit-actions">
|
|
249
260
|
<template v-if="allowDownload">
|
|
250
|
-
<a v-if="canDownload" class="kit-btn kit-btn-primary" @click="track('download')">{{ $t('kit.press.download_assets') }}</a>
|
|
261
|
+
<a v-if="canDownload" class="kit-btn kit-btn-primary" :href="zipDownloadUrl" @click="track('download')">{{ $t('kit.press.download_assets') }}</a>
|
|
251
262
|
<span v-else class="kit-btn kit-btn-primary kit-btn-disabled" :title="$t('kit.press.no_assets')">{{ $t('kit.press.no_assets') }}</span>
|
|
252
263
|
</template>
|
|
253
264
|
<a v-if="kit.press_contact_email" class="kit-btn kit-btn-ghost" :href="`mailto:${kit.press_contact_email}`" @click="track('contact')">{{ $t('kit.press.request_key') }}</a>
|
|
@@ -274,7 +285,7 @@ useSeoMeta({
|
|
|
274
285
|
<span class="kit-stickybar-name">{{ kit.name }}</span>
|
|
275
286
|
<div class="kit-stickybar-actions">
|
|
276
287
|
<template v-if="allowDownload">
|
|
277
|
-
<a v-if="canDownload" class="kit-btn kit-btn-primary kit-btn-sm" @click="track('download')">{{ $t('kit.press.download_assets') }}</a>
|
|
288
|
+
<a v-if="canDownload" class="kit-btn kit-btn-primary kit-btn-sm" :href="zipDownloadUrl" @click="track('download')">{{ $t('kit.press.download_assets') }}</a>
|
|
278
289
|
<span v-else class="kit-btn kit-btn-primary kit-btn-sm kit-btn-disabled" :title="$t('kit.press.no_assets')">{{ $t('kit.press.no_assets') }}</span>
|
|
279
290
|
</template>
|
|
280
291
|
<a v-if="kit.press_contact_email" class="kit-btn kit-btn-ghost kit-btn-sm" :href="`mailto:${kit.press_contact_email}`" @click="track('contact')">{{ $t('kit.press.request_key') }}</a>
|
|
@@ -450,7 +461,7 @@ useSeoMeta({
|
|
|
450
461
|
</section>
|
|
451
462
|
|
|
452
463
|
<footer class="kit-footer">
|
|
453
|
-
<p class="kit-footer__text">{{ $t('kit.press.made_with') }} <strong>Mundo Gamer Agency</strong></p>
|
|
464
|
+
<p v-if="!kit.is_premium" class="kit-footer__text">{{ $t('kit.press.made_with') }} <strong>Mundo Gamer Agency</strong></p>
|
|
454
465
|
<a :href="kit.create_kit_url || `${agencyBase}/presskit`" target="_blank" rel="noopener" class="kit-footer__cta">{{ $t('kit.press.create_yours') }}</a>
|
|
455
466
|
</footer>
|
|
456
467
|
</template>
|
|
Binary file
|
|
@@ -297,6 +297,8 @@ export const fetchCampaignBySlug = async (slug: string) => {
|
|
|
297
297
|
name: k.name,
|
|
298
298
|
slug: k.slug,
|
|
299
299
|
cover: k.cover_src,
|
|
300
|
+
cover_focal_x: k.cover_focal_x ?? 0.5,
|
|
301
|
+
cover_focal_y: k.cover_focal_y ?? 0.5,
|
|
300
302
|
game_cover: k.game?.url_cover_src,
|
|
301
303
|
game_slug: k.game?.slug,
|
|
302
304
|
time_to_expire: k.time_to_expire,
|
|
@@ -307,6 +309,8 @@ export const fetchCampaignBySlug = async (slug: string) => {
|
|
|
307
309
|
email: k.company?.email,
|
|
308
310
|
twitter: k.company?.twitter,
|
|
309
311
|
logo: k.company?.thumbnail_url,
|
|
312
|
+
pr_contact_name: k.supplier_pr?.name ?? null,
|
|
313
|
+
pr_contact_email: k.supplier_pr?.email ?? null,
|
|
310
314
|
status: k.status,
|
|
311
315
|
quantity: k.quantity,
|
|
312
316
|
available_count: k.available_count ?? 0,
|