@mundogamernetwork/shared-ui 1.16.21 → 1.16.23

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.
@@ -1,456 +0,0 @@
1
- <script setup lang="ts">
2
- import {
3
- fetchPublicWall,
4
- getWallPixel,
5
- requestWallPixelShareRender,
6
- pollWallPixelShareRender,
7
- } from '../../../../services/indieWallService'
8
-
9
- const { t, locale } = useI18n()
10
- const route = useRoute()
11
- const localePath = useLocalePath()
12
- const wallSlug = route.params.slug as string
13
- const pixelId = route.params.pixelId as string
14
-
15
- // SSR-safe fetch: the wall must be resolved first (its numeric id, not the
16
- // slug, is what the pixel/share-render endpoints key off — see
17
- // IndieWallController::showPixel / IndieWallPixelShareController, which
18
- // compare indie_wall_id directly and do not resolve a slug like show()/
19
- // supporters() do).
20
- const { data, error } = await useAsyncData(`mural-pixel-${wallSlug}-${pixelId}`, async () => {
21
- const wallRes = await fetchPublicWall(wallSlug)
22
- const wall = wallRes.data?.data || wallRes.data
23
- if (!wall?.id) return null
24
- const pixelRes = await getWallPixel(wall.id, pixelId)
25
- const pixel = pixelRes.data?.data || pixelRes.data
26
- if (!pixel?.id) return null
27
- return { wall, pixel }
28
- })
29
-
30
- if (error.value || !data.value?.wall || !data.value?.pixel) {
31
- throw createError({ statusCode: 404, statusMessage: 'Pixel not found' })
32
- }
33
-
34
- const wall = ref<any>(data.value.wall)
35
- const pixel = ref<any>(data.value.pixel)
36
-
37
- // ── Display helpers (duplicated from pages/mural/[slug]/index.vue rather
38
- // than shared, matching this repo's existing pattern in
39
- // components/indie-wall/MediaKitWallBlock.vue, which duplicates the same
40
- // small pure functions instead of extracting a composable). ─────────────────
41
- function supporterDisplayName(s: any): string {
42
- if (!s) return t('tv.dashboard.indie_wall.anonymous')
43
- if (s.is_anonymous) return t('tv.dashboard.indie_wall.anonymous')
44
- return s.user?.nickname
45
- || s.user?.username
46
- || s.user?.full_name
47
- || s.user?.name
48
- || s.guest_name
49
- || t('tv.dashboard.indie_wall.anonymous')
50
- }
51
-
52
- function formatMoney(amount: number, currency?: string | null) {
53
- try {
54
- return new Intl.NumberFormat(locale.value, { style: 'currency', currency: currency || 'USD' }).format(amount)
55
- } catch {
56
- return `${currency || ''} ${Number(amount).toFixed(2)}`.trim()
57
- }
58
- }
59
-
60
- const displayName = computed(() => supporterDisplayName(pixel.value))
61
-
62
- // ── SEO ───────────────────────────────────────────────────────────────────
63
- const requestUrl = useRequestURL()
64
-
65
- const seoTitle = computed(() => t('tv.dashboard.indie_wall.share_pixel_title', {
66
- name: displayName.value,
67
- wall: wall.value?.name,
68
- }))
69
- const seoDescription = computed(() =>
70
- pixel.value?.supporter_message
71
- || t('tv.dashboard.indie_wall.share_pixel_default_desc', { wall: wall.value?.name }),
72
- )
73
- // share_image_url (auto-generated branded reveal) takes priority, then the
74
- // pixel's own uploaded image, then the wall's cover/logo as a last resort.
75
- const seoImage = computed(() =>
76
- pixel.value?.share_image_url
77
- || pixel.value?.image_url
78
- || wall.value?.cover_image_url
79
- || wall.value?.logo_url
80
- || '',
81
- )
82
-
83
- // Raw useHead meta-array form — same proven pattern as
84
- // pages/mural/[slug]/index.vue (see the comment there: useSeoMeta silently
85
- // dropped og:*/twitter:*/description overrides in this app).
86
- useHead(() => ({
87
- title: seoTitle.value,
88
- link: [{ rel: 'canonical', href: requestUrl.href }],
89
- meta: [
90
- { name: 'description', content: seoDescription.value },
91
- { property: 'og:title', content: seoTitle.value },
92
- { property: 'og:description', content: seoDescription.value },
93
- ...(seoImage.value ? [{ property: 'og:image', content: seoImage.value }] : []),
94
- { property: 'og:type', content: 'website' },
95
- { property: 'og:url', content: requestUrl.href },
96
- { property: 'og:site_name', content: 'Mundo Gamer Wall' },
97
- { name: 'twitter:card', content: 'summary_large_image' },
98
- { name: 'twitter:title', content: seoTitle.value },
99
- { name: 'twitter:description', content: seoDescription.value },
100
- ...(seoImage.value ? [{ name: 'twitter:image', content: seoImage.value }] : []),
101
- ],
102
- }))
103
-
104
- // ── Share: copy / native share the page link ────────────────────────────────
105
- const shareText = computed(() => t('tv.dashboard.indie_wall.share_pixel_text', {
106
- name: displayName.value,
107
- wall: wall.value?.name,
108
- }))
109
- const shareTwitterUrl = computed(() => `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText.value)}&url=${encodeURIComponent(requestUrl.href)}`)
110
- const shareWhatsappUrl = computed(() => `https://wa.me/?text=${encodeURIComponent(`${shareText.value} ${requestUrl.href}`)}`)
111
- const shareFacebookUrl = computed(() => `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(requestUrl.href)}`)
112
-
113
- const linkCopied = ref(false)
114
- async function shareOrCopyLink() {
115
- if (typeof navigator !== 'undefined' && (navigator as any).share) {
116
- try {
117
- await (navigator as any).share({ title: seoTitle.value, text: shareText.value, url: requestUrl.href })
118
- return
119
- } catch {
120
- // user cancelled the native sheet, or it's unsupported for this
121
- // context — fall back to clipboard copy below.
122
- }
123
- }
124
- try {
125
- await navigator.clipboard.writeText(requestUrl.href)
126
- linkCopied.value = true
127
- setTimeout(() => { linkCopied.value = false }, 2000)
128
- } catch { /* clipboard unavailable */ }
129
- }
130
-
131
- // ── Download the pixel's own image ──────────────────────────────────────────
132
- const pixelDownloadImage = computed(() => pixel.value?.share_image_url || pixel.value?.image_url || '')
133
-
134
- // ── Generate-on-demand branded reveal video ─────────────────────────────────
135
- const renderState = ref<'idle' | 'rendering' | 'done' | 'failed'>('idle')
136
- const renderAssetUrl = ref<string | null>(null)
137
- let renderPollTimer: ReturnType<typeof setInterval> | null = null
138
-
139
- function stopRenderPolling() {
140
- if (renderPollTimer) {
141
- clearInterval(renderPollTimer)
142
- renderPollTimer = null
143
- }
144
- }
145
-
146
- function pollRenderJob(jobId: string) {
147
- stopRenderPolling()
148
- renderPollTimer = setInterval(async () => {
149
- try {
150
- const res = await pollWallPixelShareRender(wall.value.id, pixel.value.id, jobId)
151
- const status = res.data?.status
152
- if (status === 'done') {
153
- renderAssetUrl.value = res.data?.assetUrl || null
154
- renderState.value = renderAssetUrl.value ? 'done' : 'failed'
155
- stopRenderPolling()
156
- } else if (status === 'failed') {
157
- renderState.value = 'failed'
158
- stopRenderPolling()
159
- }
160
- // any other status (e.g. 'rendering') keeps the loop going
161
- } catch {
162
- // transient network hiccup on a single poll — keep trying
163
- }
164
- }, 3000)
165
- }
166
-
167
- async function generateReveal() {
168
- if (!wall.value?.id || !pixel.value?.id || renderState.value === 'rendering') return
169
- renderState.value = 'rendering'
170
- renderAssetUrl.value = null
171
- try {
172
- const res = await requestWallPixelShareRender(wall.value.id, pixel.value.id, 'video')
173
- const jobId = res.data?.jobId
174
- if (!jobId) throw new Error('missing_job_id')
175
- pollRenderJob(jobId)
176
- } catch {
177
- renderState.value = 'failed'
178
- }
179
- }
180
-
181
- const renderAssetIsVideo = computed(() =>
182
- !!renderAssetUrl.value && /\.(mp4|webm|mov)(\?|$)/i.test(renderAssetUrl.value),
183
- )
184
-
185
- onUnmounted(() => stopRenderPolling())
186
-
187
- // ── Back to wall ─────────────────────────────────────────────────────────────
188
- const backToWallUrl = computed(() => localePath(`/mural/${wallSlug}`))
189
- </script>
190
-
191
- <template>
192
- <div class="pixel-page">
193
- <NuxtLink :to="backToWallUrl" class="back-link">
194
- ← {{ $t('tv.dashboard.indie_wall.back_to_wall') }}
195
- </NuxtLink>
196
-
197
- <div class="pixel-card">
198
- <div class="pixel-hero" :style="{ background: pixel.background_color || '#272930' }">
199
- <img v-if="pixel.share_image_url || pixel.image_url" :src="pixel.share_image_url || pixel.image_url" alt="" />
200
- <span v-else class="pixel-initial">{{ (displayName || '?')[0].toUpperCase() }}</span>
201
- </div>
202
-
203
- <div class="pixel-body">
204
- <p class="pixel-name">{{ displayName }}</p>
205
- <p v-if="pixel.title && pixel.title !== displayName" class="pixel-title">{{ pixel.title }}</p>
206
- <p v-if="pixel.supporter_message" class="pixel-msg">"{{ pixel.supporter_message }}"</p>
207
-
208
- <div class="pixel-amount-row">
209
- <span class="pixel-amount-label">{{ $t('tv.dashboard.indie_wall.spots_unit') }}</span>
210
- <span class="pixel-amount-value">{{ formatMoney(Number(pixel.amount_paid), wall?.currency) }}</span>
211
- </div>
212
-
213
- <div class="pixel-chips">
214
- <span class="pixel-chip">{{ pixel.pixel_count }} {{ $t('tv.dashboard.indie_wall.spots_unit') }}</span>
215
- <span class="pixel-chip">{{ $t('tv.dashboard.indie_wall.mural_position') }} ({{ pixel.x }}, {{ pixel.y }})</span>
216
- </div>
217
-
218
- <a v-if="pixel.link" :href="pixel.link" target="_blank" rel="noopener" class="pixel-link">
219
- {{ pixel.link }}
220
- </a>
221
-
222
- <p class="pixel-wall-context">
223
- {{ $t('tv.dashboard.indie_wall.mural_by', { name: wall.owner_nickname || wall.owner_name || wall.name }) }}
224
- </p>
225
-
226
- <!-- ── Share ─────────────────────────────────────────────────── -->
227
- <div class="pixel-share">
228
- <p class="pixel-share-title">{{ $t('tv.dashboard.indie_wall.share_pixel_cta') }}</p>
229
-
230
- <div class="pixel-share-row">
231
- <a :href="shareTwitterUrl" target="_blank" rel="noopener" class="pixel-share-btn" :aria-label="$t('tv.dashboard.indie_wall.share_via_twitter')">𝕏</a>
232
- <a :href="shareWhatsappUrl" target="_blank" rel="noopener" class="pixel-share-btn" :aria-label="$t('tv.dashboard.indie_wall.share_via_whatsapp')">
233
- <svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor"><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>
234
- </a>
235
- <a :href="shareFacebookUrl" target="_blank" rel="noopener" class="pixel-share-btn" :aria-label="$t('tv.dashboard.indie_wall.share_via_facebook')">f</a>
236
- <button type="button" class="pixel-share-btn" @click="shareOrCopyLink" :aria-label="$t('tv.dashboard.indie_wall.share_copy_link')">
237
- {{ linkCopied ? '✓' : '🔗' }}
238
- </button>
239
- </div>
240
- <p v-if="linkCopied" class="pixel-share-copied">{{ $t('tv.dashboard.indie_wall.share_link_copied') }}</p>
241
-
242
- <a
243
- v-if="pixelDownloadImage"
244
- :href="pixelDownloadImage"
245
- download
246
- target="_blank"
247
- rel="noopener"
248
- class="pixel-share-action"
249
- >
250
- {{ $t('tv.dashboard.indie_wall.share_download_image') }}
251
- </a>
252
-
253
- <button
254
- type="button"
255
- class="pixel-share-action"
256
- :disabled="renderState === 'rendering'"
257
- @click="generateReveal"
258
- >
259
- {{ renderState === 'rendering'
260
- ? $t('tv.dashboard.indie_wall.share_generating')
261
- : $t('tv.dashboard.indie_wall.share_generate_video') }}
262
- </button>
263
-
264
- <p v-if="renderState === 'failed'" class="pixel-share-error">
265
- {{ $t('tv.dashboard.indie_wall.share_generate_failed') }}
266
- </p>
267
-
268
- <div v-if="renderState === 'done' && renderAssetUrl" class="pixel-reveal-result">
269
- <p class="pixel-reveal-title">{{ $t('tv.dashboard.indie_wall.share_video_ready') }}</p>
270
- <video v-if="renderAssetIsVideo" :src="renderAssetUrl" controls class="pixel-reveal-media" />
271
- <img v-else :src="renderAssetUrl" alt="" class="pixel-reveal-media" />
272
- <a :href="renderAssetUrl" download target="_blank" rel="noopener" class="pixel-share-action">
273
- {{ $t('tv.dashboard.indie_wall.share_download_image') }}
274
- </a>
275
- </div>
276
- </div>
277
- </div>
278
- </div>
279
- </div>
280
- </template>
281
-
282
- <style lang="scss" scoped>
283
- .pixel-page {
284
- max-width: 480px;
285
- margin: 0 auto;
286
- padding: 32px 16px 100px;
287
- }
288
-
289
- .back-link {
290
- display: inline-block;
291
- color: var(--secondary-info-fg, #aaa);
292
- font-size: 0.82rem;
293
- text-decoration: none;
294
- margin-bottom: 20px;
295
- &:hover { color: var(--title-fg, #fff); }
296
- }
297
-
298
- .pixel-card {
299
- background: #13141A;
300
- border: 1px solid rgba(107, 21, 172, 0.35);
301
- overflow: hidden;
302
- box-shadow: 0 16px 48px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(107, 21, 172, 0.1);
303
- }
304
-
305
- .pixel-hero {
306
- width: 100%;
307
- height: 260px;
308
- overflow: hidden;
309
- display: flex;
310
- align-items: center;
311
- justify-content: center;
312
- background: #0d0f13;
313
- img { max-width: 100%; max-height: 100%; object-fit: contain; }
314
- }
315
- .pixel-initial {
316
- font-size: 5rem;
317
- font-weight: 700;
318
- color: rgba(255, 255, 255, 0.5);
319
- line-height: 1;
320
- }
321
-
322
- .pixel-body { padding: 24px; }
323
- .pixel-name {
324
- font-size: 1.3rem;
325
- font-weight: 700;
326
- color: var(--title-fg, #fff);
327
- margin: 0 0 4px;
328
- }
329
- .pixel-title {
330
- color: var(--secondary-info-fg, #aaa);
331
- font-size: 0.85rem;
332
- line-height: 1.35;
333
- margin: 0 0 10px;
334
- }
335
- .pixel-msg {
336
- color: var(--secondary-info-fg, #aaa);
337
- font-size: 0.92rem;
338
- font-style: italic;
339
- line-height: 1.5;
340
- margin: 0 0 16px;
341
- }
342
- .pixel-amount-row {
343
- display: flex;
344
- align-items: center;
345
- justify-content: space-between;
346
- background: rgba(107, 21, 172, 0.12);
347
- border: 1px solid rgba(107, 21, 172, 0.25);
348
- padding: 10px 14px;
349
- margin-bottom: 12px;
350
- }
351
- .pixel-amount-label {
352
- font-size: 12px;
353
- color: var(--secondary-info-fg, #aaa);
354
- text-transform: uppercase;
355
- letter-spacing: 0.04em;
356
- }
357
- .pixel-amount-value {
358
- font-weight: 700;
359
- font-size: 1.15rem;
360
- color: var(--chip-text, #D297FF);
361
- }
362
- .pixel-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
363
- .pixel-chip {
364
- font-size: 11px;
365
- color: var(--secondary-info-fg, #aaa);
366
- background: rgba(255, 255, 255, 0.05);
367
- border: 1px solid rgba(255, 255, 255, 0.08);
368
- padding: 3px 8px;
369
- }
370
- .pixel-link {
371
- display: flex;
372
- align-items: center;
373
- gap: 6px;
374
- color: var(--chip-text, #D297FF);
375
- font-size: 0.8rem;
376
- word-break: break-all;
377
- text-decoration: none;
378
- margin-bottom: 8px;
379
- &:hover { text-decoration: underline; }
380
- }
381
- .pixel-wall-context {
382
- color: var(--secondary-info-fg, #888);
383
- font-size: 0.78rem;
384
- margin: 0;
385
- }
386
-
387
- .pixel-share {
388
- margin-top: 20px;
389
- padding-top: 20px;
390
- border-top: 1px solid #272930;
391
- }
392
- .pixel-share-title {
393
- margin: 0 0 10px;
394
- font-size: 0.85rem;
395
- font-weight: 600;
396
- color: var(--title-fg, #fff);
397
- }
398
- .pixel-share-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
399
- .pixel-share-btn {
400
- width: 36px;
401
- height: 36px;
402
- flex-shrink: 0;
403
- display: flex;
404
- align-items: center;
405
- justify-content: center;
406
- background: rgba(255, 255, 255, 0.06);
407
- border: 1px solid #272930;
408
- color: var(--title-fg, #fff);
409
- font-size: 0.9rem;
410
- font-weight: 700;
411
- cursor: pointer;
412
- text-decoration: none;
413
- transition: border-color 0.15s;
414
- &:hover { border-color: var(--chip-text, #D297FF); }
415
- }
416
- .pixel-share-copied {
417
- color: var(--chip-text, #D297FF);
418
- font-size: 0.78rem;
419
- margin: 0 0 10px;
420
- }
421
- .pixel-share-action {
422
- display: block;
423
- width: 100%;
424
- text-align: center;
425
- background: none;
426
- border: 1px solid var(--chip-text, #D297FF);
427
- color: var(--chip-text, #D297FF);
428
- font-weight: 600;
429
- font-size: 0.85rem;
430
- padding: 10px 14px;
431
- margin-top: 8px;
432
- cursor: pointer;
433
- text-decoration: none;
434
- transition: background 0.15s, color 0.15s, opacity 0.15s;
435
- &:hover:not(:disabled) { background: var(--chip-text, #D297FF); color: #13161C; }
436
- &:disabled { opacity: 0.5; cursor: not-allowed; }
437
- }
438
- .pixel-share-error {
439
- color: #ff6b6b;
440
- font-size: 0.78rem;
441
- margin: 8px 0 0;
442
- }
443
- .pixel-reveal-result { margin-top: 16px; }
444
- .pixel-reveal-title {
445
- margin: 0 0 8px;
446
- font-size: 0.82rem;
447
- font-weight: 600;
448
- color: var(--title-fg, #fff);
449
- }
450
- .pixel-reveal-media {
451
- width: 100%;
452
- display: block;
453
- background: #0d0f13;
454
- border: 1px solid #272930;
455
- }
456
- </style>
@@ -1,12 +0,0 @@
1
- <script setup lang="ts">
2
- const route = useRoute()
3
- const localePath = useLocalePath()
4
- const slug = route.params.slug as string
5
- const pixelId = route.params.pixelId as string
6
- const qs = Object.keys(route.query).length
7
- ? '?' + new URLSearchParams(route.query as Record<string, string>).toString()
8
- : ''
9
- navigateTo(localePath(`/mural/${slug}/pixel/${pixelId}`) + qs, { replace: true })
10
- </script>
11
-
12
- <template><div /></template>