@mundogamernetwork/shared-ui 1.5.0 → 1.7.0
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/MediaKit/MgMediaKitLinks.vue +122 -0
- package/components/MediaKit/MgMediaKitStore.vue +107 -0
- package/components/form/SortInput.vue +9 -2
- package/components/indie-wall/IndieWallLeaderboard.vue +17 -4
- package/locales/de.json +115 -0
- package/locales/en.json +116 -1
- package/locales/pt-BR.json +115 -0
- package/locales/ro.json +115 -0
- package/nuxt.config.ts +10 -3
- package/package.json +1 -1
- package/pages/key-campaigns/index.vue +7 -6
- package/pages/media-kit/[slug].vue +180 -39
- package/pages/mural/[slug].vue +25 -5
- package/pages/widget/wall/[id].vue +294 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="widget-root" :style="{ '--accent': alertPixel?.background_color || '#FDB215' }">
|
|
3
|
+
<!-- Canvas -->
|
|
4
|
+
<div class="widget-canvas-wrap">
|
|
5
|
+
<div ref="canvasEl" class="widget-canvas" :style="canvasStyle">
|
|
6
|
+
<div
|
|
7
|
+
v-for="pixel in visiblePixels"
|
|
8
|
+
:key="pixel.id"
|
|
9
|
+
class="widget-pixel"
|
|
10
|
+
:class="{ 'widget-pixel--new': newPixelIds.has(pixel.id) }"
|
|
11
|
+
:style="pixelStyle(pixel)"
|
|
12
|
+
>
|
|
13
|
+
<img v-if="pixel.image_url" :src="pixel.image_url" class="widget-pixel__img" alt="" />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
<!-- Alert overlay -->
|
|
19
|
+
<transition name="alert-slide">
|
|
20
|
+
<div v-if="alertPixel" :key="alertKey" class="widget-alert">
|
|
21
|
+
<div class="widget-alert__avatar">
|
|
22
|
+
<img v-if="alertPixel.buyer_avatar" :src="alertPixel.buyer_avatar" alt="" />
|
|
23
|
+
<span v-else>{{ (alertPixel.buyer_name || '?')[0].toUpperCase() }}</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div class="widget-alert__info">
|
|
26
|
+
<strong>{{ alertPixel.buyer_name || 'Anonymous' }}</strong>
|
|
27
|
+
<span v-if="alertPixel.tier_name"> · {{ alertPixel.tier_name }}</span>
|
|
28
|
+
<span> · {{ alertPixel.pixel_count || 1 }} pixel{{ (alertPixel.pixel_count || 1) > 1 ? 's' : '' }}</span>
|
|
29
|
+
<p v-if="alertPixel.message" class="widget-alert__msg">{{ alertPixel.message }}</p>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</transition>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
|
|
36
|
+
<script setup lang="ts">
|
|
37
|
+
import { fetchPublicWall, getWallSupporters, getWallEventsUrl } from '../../../services/indieWallService'
|
|
38
|
+
|
|
39
|
+
definePageMeta({ layout: 'blank' })
|
|
40
|
+
|
|
41
|
+
const route = useRoute()
|
|
42
|
+
const wallId = route.params.id as string
|
|
43
|
+
const token = (route.query.token as string) || ''
|
|
44
|
+
|
|
45
|
+
const CELL_SIZE = 12
|
|
46
|
+
|
|
47
|
+
interface Pixel {
|
|
48
|
+
id: number; x: number; y: number
|
|
49
|
+
width?: number; height?: number; pixel_count?: number
|
|
50
|
+
background_color?: string; image_url?: string
|
|
51
|
+
title?: string; message?: string
|
|
52
|
+
buyer_name?: string; buyer_avatar?: string
|
|
53
|
+
tier_name?: string; status?: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface Wall {
|
|
57
|
+
id: number; width: number; height: number; name?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const wall = ref<Wall | null>(null)
|
|
61
|
+
const pixels = ref<Pixel[]>([])
|
|
62
|
+
const newPixelIds = ref(new Set<number>())
|
|
63
|
+
const alertPixel = ref<Pixel | null>(null)
|
|
64
|
+
const alertKey = ref(0)
|
|
65
|
+
const canvasEl = ref<HTMLDivElement | null>(null)
|
|
66
|
+
|
|
67
|
+
let alertTimer: ReturnType<typeof setTimeout> | null = null
|
|
68
|
+
let audioCtx: AudioContext | null = null
|
|
69
|
+
let es: EventSource | null = null
|
|
70
|
+
|
|
71
|
+
const visiblePixels = computed(() => pixels.value.filter(p => p.status !== false))
|
|
72
|
+
|
|
73
|
+
const canvasStyle = computed(() => {
|
|
74
|
+
if (!wall.value) return {}
|
|
75
|
+
return {
|
|
76
|
+
position: 'relative' as const,
|
|
77
|
+
width: `${wall.value.width * CELL_SIZE}px`,
|
|
78
|
+
height: `${wall.value.height * CELL_SIZE}px`,
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
function pixelStyle(pixel: Pixel) {
|
|
83
|
+
const w = pixel.width ?? 1
|
|
84
|
+
const h = pixel.height ?? 1
|
|
85
|
+
return {
|
|
86
|
+
position: 'absolute' as const,
|
|
87
|
+
left: `${pixel.x * CELL_SIZE}px`,
|
|
88
|
+
top: `${pixel.y * CELL_SIZE}px`,
|
|
89
|
+
width: `${w * CELL_SIZE}px`,
|
|
90
|
+
height: `${h * CELL_SIZE}px`,
|
|
91
|
+
backgroundColor: pixel.image_url ? 'transparent' : (pixel.background_color || '#FDB215'),
|
|
92
|
+
overflow: 'hidden',
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function unlockAudio() {
|
|
97
|
+
if (!audioCtx) audioCtx = new (window.AudioContext || (window as any).webkitAudioContext)()
|
|
98
|
+
if (audioCtx.state === 'suspended') audioCtx.resume()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let audioUnlockInterval: ReturnType<typeof setInterval> | null = null
|
|
102
|
+
|
|
103
|
+
function playChime() {
|
|
104
|
+
if (!audioCtx) return
|
|
105
|
+
try {
|
|
106
|
+
const osc = audioCtx.createOscillator()
|
|
107
|
+
const gain = audioCtx.createGain()
|
|
108
|
+
osc.connect(gain)
|
|
109
|
+
gain.connect(audioCtx.destination)
|
|
110
|
+
osc.type = 'sine'
|
|
111
|
+
const t = audioCtx.currentTime
|
|
112
|
+
osc.frequency.setValueAtTime(880, t)
|
|
113
|
+
osc.frequency.linearRampToValueAtTime(1320, t + 0.12)
|
|
114
|
+
gain.gain.setValueAtTime(0.4, t)
|
|
115
|
+
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.8)
|
|
116
|
+
osc.start(t)
|
|
117
|
+
osc.stop(t + 0.8)
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function showAlert(pixel: Pixel) {
|
|
122
|
+
alertPixel.value = pixel
|
|
123
|
+
alertKey.value++
|
|
124
|
+
if (alertTimer) clearTimeout(alertTimer)
|
|
125
|
+
alertTimer = setTimeout(() => { alertPixel.value = null }, 7000)
|
|
126
|
+
playChime()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function addNewPixel(data: any) {
|
|
130
|
+
const pixel: Pixel = {
|
|
131
|
+
id: data.pixel_id || Date.now(),
|
|
132
|
+
x: data.x, y: data.y,
|
|
133
|
+
pixel_count: data.pixel_count,
|
|
134
|
+
width: data.pixel_count, height: 1,
|
|
135
|
+
background_color: data.background_color,
|
|
136
|
+
image_url: data.image_url,
|
|
137
|
+
message: data.message,
|
|
138
|
+
buyer_name: data.buyer_name,
|
|
139
|
+
tier_name: data.tier_name,
|
|
140
|
+
status: true,
|
|
141
|
+
}
|
|
142
|
+
pixels.value.push(pixel)
|
|
143
|
+
newPixelIds.value = new Set([...newPixelIds.value, pixel.id])
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
const next = new Set(newPixelIds.value)
|
|
146
|
+
next.delete(pixel.id)
|
|
147
|
+
newPixelIds.value = next
|
|
148
|
+
}, 3000)
|
|
149
|
+
showAlert(pixel)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function connectSSE() {
|
|
153
|
+
if (!token) return
|
|
154
|
+
es = new EventSource(getWallEventsUrl(wallId, token))
|
|
155
|
+
es.addEventListener('purchase', (e: MessageEvent) => {
|
|
156
|
+
try { addNewPixel(JSON.parse(e.data)) } catch {}
|
|
157
|
+
})
|
|
158
|
+
es.onerror = () => {
|
|
159
|
+
es?.close()
|
|
160
|
+
setTimeout(connectSSE, 5000)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
onMounted(async () => {
|
|
165
|
+
// OBS's embedded Chromium runs every Browser Source with autoplay unlocked
|
|
166
|
+
// by default, so resume() succeeds here with no user gesture. This widget
|
|
167
|
+
// runs unattended during a live stream — a click/keydown would never fire.
|
|
168
|
+
unlockAudio()
|
|
169
|
+
audioUnlockInterval = setInterval(() => {
|
|
170
|
+
if (!audioCtx) { unlockAudio(); return }
|
|
171
|
+
if (audioCtx.state === 'running') {
|
|
172
|
+
if (audioUnlockInterval) clearInterval(audioUnlockInterval)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
audioCtx.resume().catch(() => {})
|
|
176
|
+
}, 3000)
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const [wallRes, pixelRes] = await Promise.all([
|
|
180
|
+
fetchPublicWall(wallId),
|
|
181
|
+
getWallSupporters(wallId, { per_page: 'all' }),
|
|
182
|
+
])
|
|
183
|
+
wall.value = (wallRes as any)?.data ?? wallRes
|
|
184
|
+
pixels.value = ((pixelRes as any)?.data ?? []).filter((p: Pixel) => p.status !== false)
|
|
185
|
+
} catch {}
|
|
186
|
+
connectSSE()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
onBeforeUnmount(() => {
|
|
190
|
+
es?.close()
|
|
191
|
+
if (alertTimer) clearTimeout(alertTimer)
|
|
192
|
+
if (audioUnlockInterval) clearInterval(audioUnlockInterval)
|
|
193
|
+
})
|
|
194
|
+
</script>
|
|
195
|
+
|
|
196
|
+
<style scoped lang="scss">
|
|
197
|
+
.widget-root {
|
|
198
|
+
background: transparent;
|
|
199
|
+
width: 100vw;
|
|
200
|
+
height: 100vh;
|
|
201
|
+
overflow: hidden;
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
justify-content: center;
|
|
205
|
+
position: relative;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.widget-canvas-wrap {
|
|
209
|
+
overflow: auto;
|
|
210
|
+
max-width: 100vw;
|
|
211
|
+
max-height: 100vh;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.widget-canvas {
|
|
215
|
+
background: #0d0d0d;
|
|
216
|
+
background-image:
|
|
217
|
+
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
|
|
218
|
+
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
|
|
219
|
+
background-size: 12px 12px;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.widget-pixel {
|
|
223
|
+
box-sizing: border-box;
|
|
224
|
+
|
|
225
|
+
&__img {
|
|
226
|
+
width: 100%; height: 100%;
|
|
227
|
+
object-fit: cover; display: block;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
&--new {
|
|
231
|
+
animation: pixel-pop 0.4s ease-out, pixel-glow 2.5s ease-out forwards;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
@keyframes pixel-pop {
|
|
236
|
+
0% { transform: scale(0.5); opacity: 0.6; }
|
|
237
|
+
60% { transform: scale(1.15); }
|
|
238
|
+
100% { transform: scale(1); opacity: 1; }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@keyframes pixel-glow {
|
|
242
|
+
0% { box-shadow: 0 0 10px 3px var(--accent, #64D8FF); }
|
|
243
|
+
80% { box-shadow: 0 0 4px 1px color-mix(in srgb, var(--accent, #64D8FF) 50%, transparent); }
|
|
244
|
+
100% { box-shadow: none; }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.widget-alert {
|
|
248
|
+
position: fixed;
|
|
249
|
+
bottom: 24px;
|
|
250
|
+
left: 50%;
|
|
251
|
+
transform: translateX(-50%);
|
|
252
|
+
display: flex;
|
|
253
|
+
align-items: center;
|
|
254
|
+
gap: 12px;
|
|
255
|
+
background: rgba(13,13,13,0.92);
|
|
256
|
+
border: 1px solid var(--accent, #64D8FF);
|
|
257
|
+
padding: 12px 20px;
|
|
258
|
+
max-width: 400px;
|
|
259
|
+
z-index: 100;
|
|
260
|
+
box-shadow: 0 0 20px rgba(100, 216, 255, 0.2);
|
|
261
|
+
|
|
262
|
+
&__avatar {
|
|
263
|
+
width: 44px; height: 44px;
|
|
264
|
+
background: var(--accent, #64D8FF);
|
|
265
|
+
flex-shrink: 0;
|
|
266
|
+
display: flex; align-items: center; justify-content: center;
|
|
267
|
+
font-weight: 700; color: #000; font-size: 18px;
|
|
268
|
+
overflow: hidden;
|
|
269
|
+
|
|
270
|
+
img { width: 100%; height: 100%; object-fit: cover; }
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
&__info {
|
|
274
|
+
flex: 1;
|
|
275
|
+
color: #fff;
|
|
276
|
+
font-size: 14px;
|
|
277
|
+
line-height: 1.4;
|
|
278
|
+
|
|
279
|
+
strong { color: var(--accent, #64D8FF); }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
&__msg {
|
|
283
|
+
margin: 4px 0 0;
|
|
284
|
+
font-size: 12px;
|
|
285
|
+
color: #aaa;
|
|
286
|
+
font-style: italic;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.alert-slide-enter-active,
|
|
291
|
+
.alert-slide-leave-active { transition: all 0.3s ease; }
|
|
292
|
+
.alert-slide-enter-from,
|
|
293
|
+
.alert-slide-leave-to { opacity: 0; transform: translateX(-50%) translateY(20px); }
|
|
294
|
+
</style>
|