@mundogamernetwork/shared-ui 1.16.22 → 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.
- package/components/indie-wall/MediaKitWallBlock.vue +1 -1
- package/components/playtest/build-access/BuildAccessPanel.vue +96 -0
- package/components/playtest/build-access/BuildDownload.vue +124 -0
- package/components/playtest/build-access/NdaSign.vue +221 -0
- package/components/playtest/concept-poll/FiveSecondTestVote.vue +1 -1
- package/components/playtest/lqa/FileIssueForm.vue +9 -1
- package/composables/usePlaytestBuildAccess.ts +219 -0
- package/locales/de.json +133 -0
- package/locales/en.json +133 -0
- package/locales/es.json +133 -0
- package/locales/pt-BR.json +133 -0
- package/locales/ro.json +133 -0
- package/package.json +1 -1
- package/pages/key-campaigns/redeem-key-approved.vue +51 -0
- package/pages/mural/[slug]/index.vue +148 -60
- package/services/esignatureService.ts +56 -0
- package/services/playtestTesterService.ts +53 -0
- package/pages/mural/[slug]/pixel/[pixelId].vue +0 -460
- package/pages/wall/[slug]/pixel/[pixelId].vue +0 -12
|
@@ -1,460 +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
|
-
// indie_wall_pixels.image_url is often a base64 data: URI (the column was
|
|
76
|
-
// widened to `text` specifically for that) — a social crawler can't fetch a
|
|
77
|
-
// data: URI for og:image/twitter:image, so it's skipped here in favor of the
|
|
78
|
-
// wall's own image; the inline <img>/download link elsewhere can still use
|
|
79
|
-
// the raw value since those render in an actual browser.
|
|
80
|
-
const isShareableImageUrl = (url: unknown): url is string => typeof url === 'string' && /^https?:\/\//i.test(url)
|
|
81
|
-
const seoImage = computed(() => {
|
|
82
|
-
if (pixel.value?.share_image_url) return pixel.value.share_image_url
|
|
83
|
-
if (isShareableImageUrl(pixel.value?.image_url)) return pixel.value.image_url
|
|
84
|
-
return wall.value?.cover_image_url || wall.value?.logo_url || ''
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
// Raw useHead meta-array form — same proven pattern as
|
|
88
|
-
// pages/mural/[slug]/index.vue (see the comment there: useSeoMeta silently
|
|
89
|
-
// dropped og:*/twitter:*/description overrides in this app).
|
|
90
|
-
useHead(() => ({
|
|
91
|
-
title: seoTitle.value,
|
|
92
|
-
link: [{ rel: 'canonical', href: requestUrl.href }],
|
|
93
|
-
meta: [
|
|
94
|
-
{ name: 'description', content: seoDescription.value },
|
|
95
|
-
{ property: 'og:title', content: seoTitle.value },
|
|
96
|
-
{ property: 'og:description', content: seoDescription.value },
|
|
97
|
-
...(seoImage.value ? [{ property: 'og:image', content: seoImage.value }] : []),
|
|
98
|
-
{ property: 'og:type', content: 'website' },
|
|
99
|
-
{ property: 'og:url', content: requestUrl.href },
|
|
100
|
-
{ property: 'og:site_name', content: 'Mundo Gamer Wall' },
|
|
101
|
-
{ name: 'twitter:card', content: 'summary_large_image' },
|
|
102
|
-
{ name: 'twitter:title', content: seoTitle.value },
|
|
103
|
-
{ name: 'twitter:description', content: seoDescription.value },
|
|
104
|
-
...(seoImage.value ? [{ name: 'twitter:image', content: seoImage.value }] : []),
|
|
105
|
-
],
|
|
106
|
-
}))
|
|
107
|
-
|
|
108
|
-
// ── Share: copy / native share the page link ────────────────────────────────
|
|
109
|
-
const shareText = computed(() => t('tv.dashboard.indie_wall.share_pixel_text', {
|
|
110
|
-
name: displayName.value,
|
|
111
|
-
wall: wall.value?.name,
|
|
112
|
-
}))
|
|
113
|
-
const shareTwitterUrl = computed(() => `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText.value)}&url=${encodeURIComponent(requestUrl.href)}`)
|
|
114
|
-
const shareWhatsappUrl = computed(() => `https://wa.me/?text=${encodeURIComponent(`${shareText.value} ${requestUrl.href}`)}`)
|
|
115
|
-
const shareFacebookUrl = computed(() => `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(requestUrl.href)}`)
|
|
116
|
-
|
|
117
|
-
const linkCopied = ref(false)
|
|
118
|
-
async function shareOrCopyLink() {
|
|
119
|
-
if (typeof navigator !== 'undefined' && (navigator as any).share) {
|
|
120
|
-
try {
|
|
121
|
-
await (navigator as any).share({ title: seoTitle.value, text: shareText.value, url: requestUrl.href })
|
|
122
|
-
return
|
|
123
|
-
} catch {
|
|
124
|
-
// user cancelled the native sheet, or it's unsupported for this
|
|
125
|
-
// context — fall back to clipboard copy below.
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
try {
|
|
129
|
-
await navigator.clipboard.writeText(requestUrl.href)
|
|
130
|
-
linkCopied.value = true
|
|
131
|
-
setTimeout(() => { linkCopied.value = false }, 2000)
|
|
132
|
-
} catch { /* clipboard unavailable */ }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// ── Download the pixel's own image ──────────────────────────────────────────
|
|
136
|
-
const pixelDownloadImage = computed(() => pixel.value?.share_image_url || pixel.value?.image_url || '')
|
|
137
|
-
|
|
138
|
-
// ── Generate-on-demand branded reveal video ─────────────────────────────────
|
|
139
|
-
const renderState = ref<'idle' | 'rendering' | 'done' | 'failed'>('idle')
|
|
140
|
-
const renderAssetUrl = ref<string | null>(null)
|
|
141
|
-
let renderPollTimer: ReturnType<typeof setInterval> | null = null
|
|
142
|
-
|
|
143
|
-
function stopRenderPolling() {
|
|
144
|
-
if (renderPollTimer) {
|
|
145
|
-
clearInterval(renderPollTimer)
|
|
146
|
-
renderPollTimer = null
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function pollRenderJob(jobId: string) {
|
|
151
|
-
stopRenderPolling()
|
|
152
|
-
renderPollTimer = setInterval(async () => {
|
|
153
|
-
try {
|
|
154
|
-
const res = await pollWallPixelShareRender(wall.value.id, pixel.value.id, jobId)
|
|
155
|
-
const status = res.data?.status
|
|
156
|
-
if (status === 'done') {
|
|
157
|
-
renderAssetUrl.value = res.data?.assetUrl || null
|
|
158
|
-
renderState.value = renderAssetUrl.value ? 'done' : 'failed'
|
|
159
|
-
stopRenderPolling()
|
|
160
|
-
} else if (status === 'failed') {
|
|
161
|
-
renderState.value = 'failed'
|
|
162
|
-
stopRenderPolling()
|
|
163
|
-
}
|
|
164
|
-
// any other status (e.g. 'rendering') keeps the loop going
|
|
165
|
-
} catch {
|
|
166
|
-
// transient network hiccup on a single poll — keep trying
|
|
167
|
-
}
|
|
168
|
-
}, 3000)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
async function generateReveal() {
|
|
172
|
-
if (!wall.value?.id || !pixel.value?.id || renderState.value === 'rendering') return
|
|
173
|
-
renderState.value = 'rendering'
|
|
174
|
-
renderAssetUrl.value = null
|
|
175
|
-
try {
|
|
176
|
-
const res = await requestWallPixelShareRender(wall.value.id, pixel.value.id, 'video')
|
|
177
|
-
const jobId = res.data?.jobId
|
|
178
|
-
if (!jobId) throw new Error('missing_job_id')
|
|
179
|
-
pollRenderJob(jobId)
|
|
180
|
-
} catch {
|
|
181
|
-
renderState.value = 'failed'
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const renderAssetIsVideo = computed(() =>
|
|
186
|
-
!!renderAssetUrl.value && /\.(mp4|webm|mov)(\?|$)/i.test(renderAssetUrl.value),
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
onUnmounted(() => stopRenderPolling())
|
|
190
|
-
|
|
191
|
-
// ── Back to wall ─────────────────────────────────────────────────────────────
|
|
192
|
-
const backToWallUrl = computed(() => localePath(`/mural/${wallSlug}`))
|
|
193
|
-
</script>
|
|
194
|
-
|
|
195
|
-
<template>
|
|
196
|
-
<div class="pixel-page">
|
|
197
|
-
<NuxtLink :to="backToWallUrl" class="back-link">
|
|
198
|
-
← {{ $t('tv.dashboard.indie_wall.back_to_wall') }}
|
|
199
|
-
</NuxtLink>
|
|
200
|
-
|
|
201
|
-
<div class="pixel-card">
|
|
202
|
-
<div class="pixel-hero" :style="{ background: pixel.background_color || '#272930' }">
|
|
203
|
-
<img v-if="pixel.share_image_url || pixel.image_url" :src="pixel.share_image_url || pixel.image_url" alt="" />
|
|
204
|
-
<span v-else class="pixel-initial">{{ (displayName || '?')[0].toUpperCase() }}</span>
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
<div class="pixel-body">
|
|
208
|
-
<p class="pixel-name">{{ displayName }}</p>
|
|
209
|
-
<p v-if="pixel.title && pixel.title !== displayName" class="pixel-title">{{ pixel.title }}</p>
|
|
210
|
-
<p v-if="pixel.supporter_message" class="pixel-msg">"{{ pixel.supporter_message }}"</p>
|
|
211
|
-
|
|
212
|
-
<div class="pixel-amount-row">
|
|
213
|
-
<span class="pixel-amount-label">{{ $t('tv.dashboard.indie_wall.spots_unit') }}</span>
|
|
214
|
-
<span class="pixel-amount-value">{{ formatMoney(Number(pixel.amount_paid), wall?.currency) }}</span>
|
|
215
|
-
</div>
|
|
216
|
-
|
|
217
|
-
<div class="pixel-chips">
|
|
218
|
-
<span class="pixel-chip">{{ pixel.pixel_count }} {{ $t('tv.dashboard.indie_wall.spots_unit') }}</span>
|
|
219
|
-
<span class="pixel-chip">{{ $t('tv.dashboard.indie_wall.mural_position') }} ({{ pixel.x }}, {{ pixel.y }})</span>
|
|
220
|
-
</div>
|
|
221
|
-
|
|
222
|
-
<a v-if="pixel.link" :href="pixel.link" target="_blank" rel="noopener" class="pixel-link">
|
|
223
|
-
{{ pixel.link }}
|
|
224
|
-
</a>
|
|
225
|
-
|
|
226
|
-
<p class="pixel-wall-context">
|
|
227
|
-
{{ $t('tv.dashboard.indie_wall.mural_by', { name: wall.owner_nickname || wall.owner_name || wall.name }) }}
|
|
228
|
-
</p>
|
|
229
|
-
|
|
230
|
-
<!-- ── Share ─────────────────────────────────────────────────── -->
|
|
231
|
-
<div class="pixel-share">
|
|
232
|
-
<p class="pixel-share-title">{{ $t('tv.dashboard.indie_wall.share_pixel_cta') }}</p>
|
|
233
|
-
|
|
234
|
-
<div class="pixel-share-row">
|
|
235
|
-
<a :href="shareTwitterUrl" target="_blank" rel="noopener" class="pixel-share-btn" :aria-label="$t('tv.dashboard.indie_wall.share_via_twitter')">𝕏</a>
|
|
236
|
-
<a :href="shareWhatsappUrl" target="_blank" rel="noopener" class="pixel-share-btn" :aria-label="$t('tv.dashboard.indie_wall.share_via_whatsapp')">
|
|
237
|
-
<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>
|
|
238
|
-
</a>
|
|
239
|
-
<a :href="shareFacebookUrl" target="_blank" rel="noopener" class="pixel-share-btn" :aria-label="$t('tv.dashboard.indie_wall.share_via_facebook')">f</a>
|
|
240
|
-
<button type="button" class="pixel-share-btn" @click="shareOrCopyLink" :aria-label="$t('tv.dashboard.indie_wall.share_copy_link')">
|
|
241
|
-
{{ linkCopied ? '✓' : '🔗' }}
|
|
242
|
-
</button>
|
|
243
|
-
</div>
|
|
244
|
-
<p v-if="linkCopied" class="pixel-share-copied">{{ $t('tv.dashboard.indie_wall.share_link_copied') }}</p>
|
|
245
|
-
|
|
246
|
-
<a
|
|
247
|
-
v-if="pixelDownloadImage"
|
|
248
|
-
:href="pixelDownloadImage"
|
|
249
|
-
download
|
|
250
|
-
target="_blank"
|
|
251
|
-
rel="noopener"
|
|
252
|
-
class="pixel-share-action"
|
|
253
|
-
>
|
|
254
|
-
{{ $t('tv.dashboard.indie_wall.share_download_image') }}
|
|
255
|
-
</a>
|
|
256
|
-
|
|
257
|
-
<button
|
|
258
|
-
type="button"
|
|
259
|
-
class="pixel-share-action"
|
|
260
|
-
:disabled="renderState === 'rendering'"
|
|
261
|
-
@click="generateReveal"
|
|
262
|
-
>
|
|
263
|
-
{{ renderState === 'rendering'
|
|
264
|
-
? $t('tv.dashboard.indie_wall.share_generating')
|
|
265
|
-
: $t('tv.dashboard.indie_wall.share_generate_video') }}
|
|
266
|
-
</button>
|
|
267
|
-
|
|
268
|
-
<p v-if="renderState === 'failed'" class="pixel-share-error">
|
|
269
|
-
{{ $t('tv.dashboard.indie_wall.share_generate_failed') }}
|
|
270
|
-
</p>
|
|
271
|
-
|
|
272
|
-
<div v-if="renderState === 'done' && renderAssetUrl" class="pixel-reveal-result">
|
|
273
|
-
<p class="pixel-reveal-title">{{ $t('tv.dashboard.indie_wall.share_video_ready') }}</p>
|
|
274
|
-
<video v-if="renderAssetIsVideo" :src="renderAssetUrl" controls class="pixel-reveal-media" />
|
|
275
|
-
<img v-else :src="renderAssetUrl" alt="" class="pixel-reveal-media" />
|
|
276
|
-
<a :href="renderAssetUrl" download target="_blank" rel="noopener" class="pixel-share-action">
|
|
277
|
-
{{ $t('tv.dashboard.indie_wall.share_download_image') }}
|
|
278
|
-
</a>
|
|
279
|
-
</div>
|
|
280
|
-
</div>
|
|
281
|
-
</div>
|
|
282
|
-
</div>
|
|
283
|
-
</div>
|
|
284
|
-
</template>
|
|
285
|
-
|
|
286
|
-
<style lang="scss" scoped>
|
|
287
|
-
.pixel-page {
|
|
288
|
-
max-width: 480px;
|
|
289
|
-
margin: 0 auto;
|
|
290
|
-
padding: 32px 16px 100px;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
.back-link {
|
|
294
|
-
display: inline-block;
|
|
295
|
-
color: var(--secondary-info-fg, #aaa);
|
|
296
|
-
font-size: 0.82rem;
|
|
297
|
-
text-decoration: none;
|
|
298
|
-
margin-bottom: 20px;
|
|
299
|
-
&:hover { color: var(--title-fg, #fff); }
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
.pixel-card {
|
|
303
|
-
background: #13141A;
|
|
304
|
-
border: 1px solid rgba(107, 21, 172, 0.35);
|
|
305
|
-
overflow: hidden;
|
|
306
|
-
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.8), 0 0 0 1px rgba(107, 21, 172, 0.1);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
.pixel-hero {
|
|
310
|
-
width: 100%;
|
|
311
|
-
height: 260px;
|
|
312
|
-
overflow: hidden;
|
|
313
|
-
display: flex;
|
|
314
|
-
align-items: center;
|
|
315
|
-
justify-content: center;
|
|
316
|
-
background: #0d0f13;
|
|
317
|
-
img { max-width: 100%; max-height: 100%; object-fit: contain; }
|
|
318
|
-
}
|
|
319
|
-
.pixel-initial {
|
|
320
|
-
font-size: 5rem;
|
|
321
|
-
font-weight: 700;
|
|
322
|
-
color: rgba(255, 255, 255, 0.5);
|
|
323
|
-
line-height: 1;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
.pixel-body { padding: 24px; }
|
|
327
|
-
.pixel-name {
|
|
328
|
-
font-size: 1.3rem;
|
|
329
|
-
font-weight: 700;
|
|
330
|
-
color: var(--title-fg, #fff);
|
|
331
|
-
margin: 0 0 4px;
|
|
332
|
-
}
|
|
333
|
-
.pixel-title {
|
|
334
|
-
color: var(--secondary-info-fg, #aaa);
|
|
335
|
-
font-size: 0.85rem;
|
|
336
|
-
line-height: 1.35;
|
|
337
|
-
margin: 0 0 10px;
|
|
338
|
-
}
|
|
339
|
-
.pixel-msg {
|
|
340
|
-
color: var(--secondary-info-fg, #aaa);
|
|
341
|
-
font-size: 0.92rem;
|
|
342
|
-
font-style: italic;
|
|
343
|
-
line-height: 1.5;
|
|
344
|
-
margin: 0 0 16px;
|
|
345
|
-
}
|
|
346
|
-
.pixel-amount-row {
|
|
347
|
-
display: flex;
|
|
348
|
-
align-items: center;
|
|
349
|
-
justify-content: space-between;
|
|
350
|
-
background: rgba(107, 21, 172, 0.12);
|
|
351
|
-
border: 1px solid rgba(107, 21, 172, 0.25);
|
|
352
|
-
padding: 10px 14px;
|
|
353
|
-
margin-bottom: 12px;
|
|
354
|
-
}
|
|
355
|
-
.pixel-amount-label {
|
|
356
|
-
font-size: 12px;
|
|
357
|
-
color: var(--secondary-info-fg, #aaa);
|
|
358
|
-
text-transform: uppercase;
|
|
359
|
-
letter-spacing: 0.04em;
|
|
360
|
-
}
|
|
361
|
-
.pixel-amount-value {
|
|
362
|
-
font-weight: 700;
|
|
363
|
-
font-size: 1.15rem;
|
|
364
|
-
color: var(--chip-text, #D297FF);
|
|
365
|
-
}
|
|
366
|
-
.pixel-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
|
|
367
|
-
.pixel-chip {
|
|
368
|
-
font-size: 11px;
|
|
369
|
-
color: var(--secondary-info-fg, #aaa);
|
|
370
|
-
background: rgba(255, 255, 255, 0.05);
|
|
371
|
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
372
|
-
padding: 3px 8px;
|
|
373
|
-
}
|
|
374
|
-
.pixel-link {
|
|
375
|
-
display: flex;
|
|
376
|
-
align-items: center;
|
|
377
|
-
gap: 6px;
|
|
378
|
-
color: var(--chip-text, #D297FF);
|
|
379
|
-
font-size: 0.8rem;
|
|
380
|
-
word-break: break-all;
|
|
381
|
-
text-decoration: none;
|
|
382
|
-
margin-bottom: 8px;
|
|
383
|
-
&:hover { text-decoration: underline; }
|
|
384
|
-
}
|
|
385
|
-
.pixel-wall-context {
|
|
386
|
-
color: var(--secondary-info-fg, #888);
|
|
387
|
-
font-size: 0.78rem;
|
|
388
|
-
margin: 0;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
.pixel-share {
|
|
392
|
-
margin-top: 20px;
|
|
393
|
-
padding-top: 20px;
|
|
394
|
-
border-top: 1px solid #272930;
|
|
395
|
-
}
|
|
396
|
-
.pixel-share-title {
|
|
397
|
-
margin: 0 0 10px;
|
|
398
|
-
font-size: 0.85rem;
|
|
399
|
-
font-weight: 600;
|
|
400
|
-
color: var(--title-fg, #fff);
|
|
401
|
-
}
|
|
402
|
-
.pixel-share-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
|
403
|
-
.pixel-share-btn {
|
|
404
|
-
width: 36px;
|
|
405
|
-
height: 36px;
|
|
406
|
-
flex-shrink: 0;
|
|
407
|
-
display: flex;
|
|
408
|
-
align-items: center;
|
|
409
|
-
justify-content: center;
|
|
410
|
-
background: rgba(255, 255, 255, 0.06);
|
|
411
|
-
border: 1px solid #272930;
|
|
412
|
-
color: var(--title-fg, #fff);
|
|
413
|
-
font-size: 0.9rem;
|
|
414
|
-
font-weight: 700;
|
|
415
|
-
cursor: pointer;
|
|
416
|
-
text-decoration: none;
|
|
417
|
-
transition: border-color 0.15s;
|
|
418
|
-
&:hover { border-color: var(--chip-text, #D297FF); }
|
|
419
|
-
}
|
|
420
|
-
.pixel-share-copied {
|
|
421
|
-
color: var(--chip-text, #D297FF);
|
|
422
|
-
font-size: 0.78rem;
|
|
423
|
-
margin: 0 0 10px;
|
|
424
|
-
}
|
|
425
|
-
.pixel-share-action {
|
|
426
|
-
display: block;
|
|
427
|
-
width: 100%;
|
|
428
|
-
text-align: center;
|
|
429
|
-
background: none;
|
|
430
|
-
border: 1px solid var(--chip-text, #D297FF);
|
|
431
|
-
color: var(--chip-text, #D297FF);
|
|
432
|
-
font-weight: 600;
|
|
433
|
-
font-size: 0.85rem;
|
|
434
|
-
padding: 10px 14px;
|
|
435
|
-
margin-top: 8px;
|
|
436
|
-
cursor: pointer;
|
|
437
|
-
text-decoration: none;
|
|
438
|
-
transition: background 0.15s, color 0.15s, opacity 0.15s;
|
|
439
|
-
&:hover:not(:disabled) { background: var(--chip-text, #D297FF); color: #13161C; }
|
|
440
|
-
&:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
441
|
-
}
|
|
442
|
-
.pixel-share-error {
|
|
443
|
-
color: #ff6b6b;
|
|
444
|
-
font-size: 0.78rem;
|
|
445
|
-
margin: 8px 0 0;
|
|
446
|
-
}
|
|
447
|
-
.pixel-reveal-result { margin-top: 16px; }
|
|
448
|
-
.pixel-reveal-title {
|
|
449
|
-
margin: 0 0 8px;
|
|
450
|
-
font-size: 0.82rem;
|
|
451
|
-
font-weight: 600;
|
|
452
|
-
color: var(--title-fg, #fff);
|
|
453
|
-
}
|
|
454
|
-
.pixel-reveal-media {
|
|
455
|
-
width: 100%;
|
|
456
|
-
display: block;
|
|
457
|
-
background: #0d0f13;
|
|
458
|
-
border: 1px solid #272930;
|
|
459
|
-
}
|
|
460
|
-
</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>
|