@mundogamernetwork/shared-ui 1.6.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.
@@ -93,9 +93,9 @@ const save = () => {
93
93
  </template>
94
94
 
95
95
  <style scoped lang="scss">
96
- .mk-links-editor { border: 1px solid rgba(140, 140, 160, .3); padding: 20px; background: rgba(255, 255, 255, .02); }
96
+ .mk-links-editor { border: 1px solid rgba(140, 140, 160, .3); padding: 20px; background: rgba(255, 255, 255, .02); color: #f2f3f5; }
97
97
  .mk-links-head { display: flex; justify-content: space-between; gap: 20px; align-items: start; }
98
- .mk-links-head h3 { margin: 0 0 5px; font-size: 17px; }
98
+ .mk-links-head h3 { margin: 0 0 5px; color: #f2f3f5; font-size: 17px; }
99
99
  .mk-links-head p, .mk-links-empty { margin: 0; color: #9292a4; font-size: 13px; line-height: 1.5; }
100
100
  .mk-link-add, .mk-link-save { border: 1px solid #ffb000; background: #ffb000; color: #090909; min-height: 38px; padding: 0 16px; font-weight: 750; cursor: pointer; white-space: nowrap; }
101
101
  button:disabled { opacity: .42; cursor: not-allowed; }
@@ -103,7 +103,8 @@ button:disabled { opacity: .42; cursor: not-allowed; }
103
103
  .mk-link-row { display: grid; grid-template-columns: 30px minmax(150px, .75fr) minmax(220px, 1.4fr) auto; gap: 10px; align-items: end; padding: 12px; border: 1px solid rgba(140, 140, 160, .25); }
104
104
  .mk-link-number { align-self: center; color: #ffb000; font-weight: 800; text-align: center; }
105
105
  .mk-link-row label { display: grid; gap: 6px; color: #9292a4; font-size: 11px; text-transform: uppercase; letter-spacing: .08em; }
106
- .mk-link-row input { min-width: 0; height: 40px; border: 1px solid rgba(140, 140, 160, .35); background: rgba(0, 0, 0, .2); color: inherit; padding: 0 11px; outline: none; }
106
+ .mk-link-row input { min-width: 0; height: 40px; border: 1px solid rgba(140, 140, 160, .35); background: rgba(0, 0, 0, .2); color: #f2f3f5; padding: 0 11px; outline: none; }
107
+ .mk-link-row input::placeholder { color: #6f7480; }
107
108
  .mk-link-row input:focus { border-color: #ffb000; }
108
109
  .mk-link-actions { display: flex; gap: 5px; }
109
110
  .mk-link-actions button { width: 36px; height: 40px; border: 1px solid rgba(140, 140, 160, .35); background: transparent; color: inherit; cursor: pointer; }
@@ -26,9 +26,7 @@ const ranked = computed<RankEntry[]>(() => {
26
26
  const key = userId ? `u:${userId}` : email ? `e:${email}` : null
27
27
  if (!key) continue
28
28
 
29
- const name = s.is_anonymous
30
- ? t('tv.dashboard.indie_wall.anonymous')
31
- : (s.title || s.user?.name || s.guest_name || 'Guest')
29
+ const name = supporterDisplayName(s)
32
30
 
33
31
  if (map.has(key)) {
34
32
  const entry = map.get(key)!
@@ -38,7 +36,7 @@ const ranked = computed<RankEntry[]>(() => {
38
36
  map.set(key, {
39
37
  key,
40
38
  name,
41
- avatarUrl: s.user?.avatar_url || null,
39
+ avatarUrl: supporterAvatarUrl(s),
42
40
  totalPixels: s.pixel_count || 1,
43
41
  purchases: 1,
44
42
  position: 0,
@@ -57,6 +55,21 @@ const visible = computed(() => showAll.value ? ranked.value : ranked.value.slice
57
55
  function initials(name: string): string {
58
56
  return name.trim().split(/\s+/).slice(0, 2).map(w => w[0]).join('').toUpperCase()
59
57
  }
58
+
59
+ function supporterDisplayName(s: any): string {
60
+ if (s.is_anonymous) return t('tv.dashboard.indie_wall.anonymous')
61
+ return s.user?.nickname
62
+ || s.user?.username
63
+ || s.user?.full_name
64
+ || s.user?.name
65
+ || s.guest_name
66
+ || 'Guest'
67
+ }
68
+
69
+ function supporterAvatarUrl(s: any): string | null {
70
+ if (s.is_anonymous) return null
71
+ return s.user?.avatar_url || s.user?.profile_image || s.user?.avatar || s.user?.image || null
72
+ }
60
73
  </script>
61
74
 
62
75
  <template>
package/locales/de.json CHANGED
@@ -240,5 +240,120 @@
240
240
  "common": {
241
241
  "video_thumbnail": "Video-Vorschaubild",
242
242
  "expanded_image": "Vergrößertes Bild"
243
+ },
244
+ "tv": {
245
+ "dashboard": {
246
+ "indie_wall": {
247
+ "about_section": "Über die Wall",
248
+ "anonymous": "Anonym",
249
+ "cancel": "Abbrechen",
250
+ "confirm_support": "Unterstützung bestätigen",
251
+ "cta_create_btn": "Create my account",
252
+ "cta_create_desc": "Create an account to track your support, receive updates, and join other walls.",
253
+ "cta_create_skip": "Continue without an account",
254
+ "cta_create_title": "Want to save your support?",
255
+ "drag_to_crop": "Drag to crop",
256
+ "error_area_occupied": "This area is already taken. Choose another space on the wall.",
257
+ "error_gateway": "Could not start payment. Try again.",
258
+ "error_save": "Could not save your support. Try again.",
259
+ "filled": "Gefüllt",
260
+ "footer_create_cta": "Create your own wall",
261
+ "footer_made_with": "Made with",
262
+ "goal_card_cta": "Dieses Ziel unterstützen",
263
+ "goal_card_goal": "Goal",
264
+ "goal_card_missing": "es fehlen {count} Pixel bis zur nächsten Lieferung",
265
+ "goal_card_missing_amount": "es fehlen {amount} bis zur nächsten Lieferung",
266
+ "goal_card_result": "Result",
267
+ "goal_card_select": "Select goal",
268
+ "goal_card_you_help": "You help",
269
+ "goals": "Ziele",
270
+ "guest_email_label": "Your email",
271
+ "guest_email_placeholder": "you@email.com",
272
+ "guest_intro": "You can support without creating an account. Use a name and email to receive confirmation.",
273
+ "guest_name_label": "Public name",
274
+ "guest_name_placeholder": "How do you want to appear on the wall?",
275
+ "just_supported": "Support received",
276
+ "leaderboard_pixels": "pixels",
277
+ "leaderboard_purchases": "support",
278
+ "leaderboard_purchases_plural": "supports",
279
+ "leaderboard_show_less": "Show less",
280
+ "leaderboard_show_more": "Show more",
281
+ "leaderboard_title": "Supporter",
282
+ "leaderboard_title_span": "leaderboard",
283
+ "message": "Message",
284
+ "message_placeholder": "Leave a message for the creator",
285
+ "mural_by": "by {name}",
286
+ "mural_click_hint": "Click a pixel to see who supported",
287
+ "mural_grid": "Wall",
288
+ "mural_legend_free": "Free",
289
+ "mural_legend_taken": "Taken",
290
+ "mural_position": "Position",
291
+ "mural_support_now": "Support now",
292
+ "mural_title_field": "Support title",
293
+ "mural_title_placeholder": "Ex: First pixels of the project",
294
+ "not_found": "Wall not found.",
295
+ "pay_agree": "I agree to the support and billing terms.",
296
+ "pay_agree_rights": "I understand the image and message may appear publicly on the wall.",
297
+ "pay_paypal": "PayPal",
298
+ "pay_paypal_meta": "Secure payment through PayPal",
299
+ "pay_pix": "Pix",
300
+ "pay_pix_soon": "Coming soon",
301
+ "pay_stripe": "Card",
302
+ "pay_stripe_meta": "Secure payment through Stripe",
303
+ "pixels_available": "Available pixels",
304
+ "processing_close": "Close",
305
+ "processing_desc": "We are confirming your support. This may take a few seconds.",
306
+ "processing_timeout_desc": "Confirmation is taking longer than usual. You can close this and check again later.",
307
+ "processing_timeout_title": "Still processing",
308
+ "processing_title": "Processing support",
309
+ "raised": "Gesammelt",
310
+ "reached": "Erreicht",
311
+ "recent_supporters": "Recent supporters",
312
+ "share_copy_link": "Copy link",
313
+ "share_join_cta": "Diese Wall ebenfalls unterstützen",
314
+ "share_pixel_cta": "Diese Unterstützung teilen",
315
+ "share_pixel_default_desc": "See who is supporting the {wall} wall.",
316
+ "share_pixel_text": "{name} supported the {wall} wall. Join in and support too.",
317
+ "share_pixel_title": "{name} supported {wall}",
318
+ "share_via_facebook": "Share on Facebook",
319
+ "share_via_twitter": "Share on X",
320
+ "share_via_whatsapp": "Share on WhatsApp",
321
+ "sold": "sold",
322
+ "spots_unit": "Pixel",
323
+ "step_back": "Back",
324
+ "step_block_auto": "Choose automatically",
325
+ "step_block_confirm": "Confirm position",
326
+ "step_block_help": "Choose where your {count} pixels will appear on the wall.",
327
+ "step_block_manual": "Choose manually",
328
+ "step_block_quantity": "Quantity",
329
+ "step_block_shape": "Shape",
330
+ "step_block_title": "Choose your space",
331
+ "step_block_total": "{total} pixels",
332
+ "step_customize_continue": "Continue",
333
+ "step_customize_help": "Upload your image, title, and message to appear on the wall.",
334
+ "step_customize_image": "Support image",
335
+ "step_customize_image_hint": "Use a square image or crop it before continuing.",
336
+ "step_customize_link": "Optional link",
337
+ "step_customize_link_error": "Enter a valid link.",
338
+ "step_customize_remove_image": "Remove image",
339
+ "step_customize_replace_image": "Replace image",
340
+ "step_customize_title": "Customize your support",
341
+ "step_customize_upload_image": "Upload image",
342
+ "step_goal_help": "Choose a goal to direct your support.",
343
+ "step_goal_skip_desc": "Your support goes to the general wall without linking to a specific goal.",
344
+ "step_goal_skip_title": "Support without a goal",
345
+ "step_goal_title": "Choose a goal",
346
+ "step_package_custom_cta": "Set quantity",
347
+ "step_package_custom_desc": "Choose how many pixels you want to buy.",
348
+ "step_package_custom_title": "Custom",
349
+ "step_package_help": "Choose a pixel package for the wall.",
350
+ "step_package_title": "Choose your package",
351
+ "step_pay_method": "Payment method",
352
+ "step_pay_title": "Payment",
353
+ "step_pay_total": "Total",
354
+ "supporters": "Unterstützer",
355
+ "tiers": "Packages"
356
+ }
357
+ }
243
358
  }
244
359
  }
package/locales/en.json CHANGED
@@ -240,5 +240,120 @@
240
240
  "common": {
241
241
  "video_thumbnail": "Video thumbnail",
242
242
  "expanded_image": "Expanded image"
243
+ },
244
+ "tv": {
245
+ "dashboard": {
246
+ "indie_wall": {
247
+ "about_section": "About the wall",
248
+ "anonymous": "Anonymous",
249
+ "cancel": "Cancel",
250
+ "confirm_support": "Confirm support",
251
+ "cta_create_btn": "Create my account",
252
+ "cta_create_desc": "Create an account to track your support, receive updates, and join other walls.",
253
+ "cta_create_skip": "Continue without an account",
254
+ "cta_create_title": "Want to save your support?",
255
+ "drag_to_crop": "Drag to crop",
256
+ "error_area_occupied": "This area is already taken. Choose another space on the wall.",
257
+ "error_gateway": "Could not start payment. Try again.",
258
+ "error_save": "Could not save your support. Try again.",
259
+ "filled": "Filled",
260
+ "footer_create_cta": "Create your own wall",
261
+ "footer_made_with": "Made with",
262
+ "goal_card_cta": "Support this goal",
263
+ "goal_card_goal": "Goal",
264
+ "goal_card_missing": "missing {count} pixels for the next delivery",
265
+ "goal_card_missing_amount": "missing {amount} for the next delivery",
266
+ "goal_card_result": "Result",
267
+ "goal_card_select": "Select goal",
268
+ "goal_card_you_help": "You help",
269
+ "goals": "Goals",
270
+ "guest_email_label": "Your email",
271
+ "guest_email_placeholder": "you@email.com",
272
+ "guest_intro": "You can support without creating an account. Use a name and email to receive confirmation.",
273
+ "guest_name_label": "Public name",
274
+ "guest_name_placeholder": "How do you want to appear on the wall?",
275
+ "just_supported": "Support received",
276
+ "leaderboard_pixels": "pixels",
277
+ "leaderboard_purchases": "support",
278
+ "leaderboard_purchases_plural": "supports",
279
+ "leaderboard_show_less": "Show less",
280
+ "leaderboard_show_more": "Show more",
281
+ "leaderboard_title": "Supporter",
282
+ "leaderboard_title_span": "leaderboard",
283
+ "message": "Message",
284
+ "message_placeholder": "Leave a message for the creator",
285
+ "mural_by": "by {name}",
286
+ "mural_click_hint": "Click a pixel to see who supported",
287
+ "mural_grid": "Wall",
288
+ "mural_legend_free": "Free",
289
+ "mural_legend_taken": "Taken",
290
+ "mural_position": "Position",
291
+ "mural_support_now": "Support now",
292
+ "mural_title_field": "Support title",
293
+ "mural_title_placeholder": "Ex: First pixels of the project",
294
+ "not_found": "Wall not found.",
295
+ "pay_agree": "I agree to the support and billing terms.",
296
+ "pay_agree_rights": "I understand the image and message may appear publicly on the wall.",
297
+ "pay_paypal": "PayPal",
298
+ "pay_paypal_meta": "Secure payment through PayPal",
299
+ "pay_pix": "Pix",
300
+ "pay_pix_soon": "Coming soon",
301
+ "pay_stripe": "Card",
302
+ "pay_stripe_meta": "Secure payment through Stripe",
303
+ "pixels_available": "Available pixels",
304
+ "processing_close": "Close",
305
+ "processing_desc": "We are confirming your support. This may take a few seconds.",
306
+ "processing_timeout_desc": "Confirmation is taking longer than usual. You can close this and check again later.",
307
+ "processing_timeout_title": "Still processing",
308
+ "processing_title": "Processing support",
309
+ "raised": "Raised",
310
+ "reached": "Reached",
311
+ "recent_supporters": "Recent supporters",
312
+ "share_copy_link": "Copy link",
313
+ "share_join_cta": "Support this wall too",
314
+ "share_pixel_cta": "Share this support",
315
+ "share_pixel_default_desc": "See who is supporting the {wall} wall.",
316
+ "share_pixel_text": "{name} supported the {wall} wall. Join in and support too.",
317
+ "share_pixel_title": "{name} supported {wall}",
318
+ "share_via_facebook": "Share on Facebook",
319
+ "share_via_twitter": "Share on X",
320
+ "share_via_whatsapp": "Share on WhatsApp",
321
+ "sold": "sold",
322
+ "spots_unit": "pixels",
323
+ "step_back": "Back",
324
+ "step_block_auto": "Choose automatically",
325
+ "step_block_confirm": "Confirm position",
326
+ "step_block_help": "Choose where your {count} pixels will appear on the wall.",
327
+ "step_block_manual": "Choose manually",
328
+ "step_block_quantity": "Quantity",
329
+ "step_block_shape": "Shape",
330
+ "step_block_title": "Choose your space",
331
+ "step_block_total": "{total} pixels",
332
+ "step_customize_continue": "Continue",
333
+ "step_customize_help": "Upload your image, title, and message to appear on the wall.",
334
+ "step_customize_image": "Support image",
335
+ "step_customize_image_hint": "Use a square image or crop it before continuing.",
336
+ "step_customize_link": "Optional link",
337
+ "step_customize_link_error": "Enter a valid link.",
338
+ "step_customize_remove_image": "Remove image",
339
+ "step_customize_replace_image": "Replace image",
340
+ "step_customize_title": "Customize your support",
341
+ "step_customize_upload_image": "Upload image",
342
+ "step_goal_help": "Choose a goal to direct your support.",
343
+ "step_goal_skip_desc": "Your support goes to the general wall without linking to a specific goal.",
344
+ "step_goal_skip_title": "Support without a goal",
345
+ "step_goal_title": "Choose a goal",
346
+ "step_package_custom_cta": "Set quantity",
347
+ "step_package_custom_desc": "Choose how many pixels you want to buy.",
348
+ "step_package_custom_title": "Custom",
349
+ "step_package_help": "Choose a pixel package for the wall.",
350
+ "step_package_title": "Choose your package",
351
+ "step_pay_method": "Payment method",
352
+ "step_pay_title": "Payment",
353
+ "step_pay_total": "Total",
354
+ "supporters": "Supporters",
355
+ "tiers": "Packages"
356
+ }
357
+ }
243
358
  }
244
- }
359
+ }
@@ -240,5 +240,120 @@
240
240
  "common": {
241
241
  "video_thumbnail": "Miniatura do vídeo",
242
242
  "expanded_image": "Imagem ampliada"
243
+ },
244
+ "tv": {
245
+ "dashboard": {
246
+ "indie_wall": {
247
+ "about_section": "Sobre o mural",
248
+ "anonymous": "Anônimo",
249
+ "cancel": "Cancelar",
250
+ "confirm_support": "Confirmar apoio",
251
+ "cta_create_btn": "Criar minha conta",
252
+ "cta_create_desc": "Crie uma conta para acompanhar seu apoio, receber atualizações e participar de outros murais.",
253
+ "cta_create_skip": "Continuar sem criar conta",
254
+ "cta_create_title": "Quer salvar seu apoio?",
255
+ "drag_to_crop": "Arraste para recortar",
256
+ "error_area_occupied": "Essa área já foi ocupada. Escolha outro espaço no mural.",
257
+ "error_gateway": "Não foi possível iniciar o pagamento. Tente novamente.",
258
+ "error_save": "Não foi possível salvar seu apoio. Tente novamente.",
259
+ "filled": "Preenchido",
260
+ "footer_create_cta": "Crie seu próprio mural",
261
+ "footer_made_with": "Feito com",
262
+ "goal_card_cta": "Apoiar esta meta",
263
+ "goal_card_goal": "Meta",
264
+ "goal_card_missing": "faltam {count} pixels para a próxima entrega",
265
+ "goal_card_missing_amount": "faltam {amount} para a próxima entrega",
266
+ "goal_card_result": "Resultado",
267
+ "goal_card_select": "Selecionar meta",
268
+ "goal_card_you_help": "Você ajuda",
269
+ "goals": "Metas",
270
+ "guest_email_label": "Seu e-mail",
271
+ "guest_email_placeholder": "voce@email.com",
272
+ "guest_intro": "Você pode apoiar sem criar conta. Use um nome e e-mail para receber a confirmação.",
273
+ "guest_name_label": "Nome público",
274
+ "guest_name_placeholder": "Como você quer aparecer no mural?",
275
+ "just_supported": "Apoio recebido",
276
+ "leaderboard_pixels": "pixels",
277
+ "leaderboard_purchases": "apoio",
278
+ "leaderboard_purchases_plural": "apoios",
279
+ "leaderboard_show_less": "Ver menos",
280
+ "leaderboard_show_more": "Ver mais",
281
+ "leaderboard_title": "Ranking",
282
+ "leaderboard_title_span": "de apoiadores",
283
+ "message": "Mensagem",
284
+ "message_placeholder": "Deixe uma mensagem para o criador",
285
+ "mural_by": "por {name}",
286
+ "mural_click_hint": "Clique em um pixel para ver quem apoiou",
287
+ "mural_grid": "Mural",
288
+ "mural_legend_free": "Livre",
289
+ "mural_legend_taken": "Ocupado",
290
+ "mural_position": "Posição",
291
+ "mural_support_now": "Apoiar agora",
292
+ "mural_title_field": "Título do apoio",
293
+ "mural_title_placeholder": "Ex: Primeiros pixels do projeto",
294
+ "not_found": "Mural não encontrado.",
295
+ "pay_agree": "Concordo com os termos de apoio e cobrança.",
296
+ "pay_agree_rights": "Entendo que a imagem e mensagem podem aparecer publicamente no mural.",
297
+ "pay_paypal": "PayPal",
298
+ "pay_paypal_meta": "Pagamento seguro via PayPal",
299
+ "pay_pix": "Pix",
300
+ "pay_pix_soon": "Disponível em breve",
301
+ "pay_stripe": "Cartão",
302
+ "pay_stripe_meta": "Pagamento seguro via Stripe",
303
+ "pixels_available": "Pixels disponíveis",
304
+ "processing_close": "Fechar",
305
+ "processing_desc": "Estamos confirmando seu apoio. Isso pode levar alguns segundos.",
306
+ "processing_timeout_desc": "A confirmação está demorando mais que o normal. Você pode fechar e conferir novamente depois.",
307
+ "processing_timeout_title": "Ainda processando",
308
+ "processing_title": "Processando apoio",
309
+ "raised": "Arrecadado",
310
+ "reached": "Concluída",
311
+ "recent_supporters": "Apoiadores recentes",
312
+ "share_copy_link": "Copiar link",
313
+ "share_join_cta": "Apoiar este mural também",
314
+ "share_pixel_cta": "Compartilhe este apoio",
315
+ "share_pixel_default_desc": "Veja quem está apoiando o mural {wall}.",
316
+ "share_pixel_text": "{name} apoiou o mural {wall}. Venha apoiar também.",
317
+ "share_pixel_title": "{name} apoiou {wall}",
318
+ "share_via_facebook": "Compartilhar no Facebook",
319
+ "share_via_twitter": "Compartilhar no X",
320
+ "share_via_whatsapp": "Compartilhar no WhatsApp",
321
+ "sold": "vendidos",
322
+ "spots_unit": "pixels",
323
+ "step_back": "Voltar",
324
+ "step_block_auto": "Escolher automaticamente",
325
+ "step_block_confirm": "Confirmar posição",
326
+ "step_block_help": "Escolha onde seus {count} pixels aparecerão no mural.",
327
+ "step_block_manual": "Escolher manualmente",
328
+ "step_block_quantity": "Quantidade",
329
+ "step_block_shape": "Formato",
330
+ "step_block_title": "Escolha seu espaço",
331
+ "step_block_total": "{total} pixels",
332
+ "step_customize_continue": "Continuar",
333
+ "step_customize_help": "Envie sua imagem, título e mensagem para aparecer no mural.",
334
+ "step_customize_image": "Imagem do apoio",
335
+ "step_customize_image_hint": "Use uma imagem quadrada ou recorte antes de continuar.",
336
+ "step_customize_link": "Link opcional",
337
+ "step_customize_link_error": "Informe um link válido.",
338
+ "step_customize_remove_image": "Remover imagem",
339
+ "step_customize_replace_image": "Trocar imagem",
340
+ "step_customize_title": "Personalize seu apoio",
341
+ "step_customize_upload_image": "Enviar imagem",
342
+ "step_goal_help": "Escolha uma meta para direcionar seu apoio.",
343
+ "step_goal_skip_desc": "Seu apoio entra no mural geral sem vincular a uma meta específica.",
344
+ "step_goal_skip_title": "Apoiar sem meta",
345
+ "step_goal_title": "Escolha uma meta",
346
+ "step_package_custom_cta": "Definir quantidade",
347
+ "step_package_custom_desc": "Escolha quantos pixels quer comprar.",
348
+ "step_package_custom_title": "Personalizado",
349
+ "step_package_help": "Escolha um pacote de pixels para ocupar no mural.",
350
+ "step_package_title": "Escolha seu pacote",
351
+ "step_pay_method": "Forma de pagamento",
352
+ "step_pay_title": "Pagamento",
353
+ "step_pay_total": "Total",
354
+ "supporters": "Apoiadores",
355
+ "tiers": "Pacotes"
356
+ }
357
+ }
243
358
  }
244
359
  }
package/locales/ro.json CHANGED
@@ -240,5 +240,120 @@
240
240
  "common": {
241
241
  "video_thumbnail": "Miniatură video",
242
242
  "expanded_image": "Imagine mărită"
243
+ },
244
+ "tv": {
245
+ "dashboard": {
246
+ "indie_wall": {
247
+ "about_section": "Despre mural",
248
+ "anonymous": "Anonim",
249
+ "cancel": "Anulează",
250
+ "confirm_support": "Confirmă susținerea",
251
+ "cta_create_btn": "Create my account",
252
+ "cta_create_desc": "Create an account to track your support, receive updates, and join other walls.",
253
+ "cta_create_skip": "Continue without an account",
254
+ "cta_create_title": "Want to save your support?",
255
+ "drag_to_crop": "Drag to crop",
256
+ "error_area_occupied": "This area is already taken. Choose another space on the wall.",
257
+ "error_gateway": "Could not start payment. Try again.",
258
+ "error_save": "Could not save your support. Try again.",
259
+ "filled": "Completat",
260
+ "footer_create_cta": "Create your own wall",
261
+ "footer_made_with": "Made with",
262
+ "goal_card_cta": "Susține acest obiectiv",
263
+ "goal_card_goal": "Goal",
264
+ "goal_card_missing": "mai lipsesc {count} pixeli pentru următoarea livrare",
265
+ "goal_card_missing_amount": "mai lipsesc {amount} pentru următoarea livrare",
266
+ "goal_card_result": "Result",
267
+ "goal_card_select": "Select goal",
268
+ "goal_card_you_help": "You help",
269
+ "goals": "Obiective",
270
+ "guest_email_label": "Your email",
271
+ "guest_email_placeholder": "you@email.com",
272
+ "guest_intro": "You can support without creating an account. Use a name and email to receive confirmation.",
273
+ "guest_name_label": "Public name",
274
+ "guest_name_placeholder": "How do you want to appear on the wall?",
275
+ "just_supported": "Support received",
276
+ "leaderboard_pixels": "pixels",
277
+ "leaderboard_purchases": "support",
278
+ "leaderboard_purchases_plural": "supports",
279
+ "leaderboard_show_less": "Show less",
280
+ "leaderboard_show_more": "Show more",
281
+ "leaderboard_title": "Supporter",
282
+ "leaderboard_title_span": "leaderboard",
283
+ "message": "Message",
284
+ "message_placeholder": "Leave a message for the creator",
285
+ "mural_by": "by {name}",
286
+ "mural_click_hint": "Click a pixel to see who supported",
287
+ "mural_grid": "Wall",
288
+ "mural_legend_free": "Free",
289
+ "mural_legend_taken": "Taken",
290
+ "mural_position": "Poziție",
291
+ "mural_support_now": "Support now",
292
+ "mural_title_field": "Support title",
293
+ "mural_title_placeholder": "Ex: First pixels of the project",
294
+ "not_found": "Wall not found.",
295
+ "pay_agree": "I agree to the support and billing terms.",
296
+ "pay_agree_rights": "I understand the image and message may appear publicly on the wall.",
297
+ "pay_paypal": "PayPal",
298
+ "pay_paypal_meta": "Secure payment through PayPal",
299
+ "pay_pix": "Pix",
300
+ "pay_pix_soon": "Coming soon",
301
+ "pay_stripe": "Card",
302
+ "pay_stripe_meta": "Secure payment through Stripe",
303
+ "pixels_available": "Available pixels",
304
+ "processing_close": "Close",
305
+ "processing_desc": "We are confirming your support. This may take a few seconds.",
306
+ "processing_timeout_desc": "Confirmation is taking longer than usual. You can close this and check again later.",
307
+ "processing_timeout_title": "Still processing",
308
+ "processing_title": "Processing support",
309
+ "raised": "Strâns",
310
+ "reached": "Atins",
311
+ "recent_supporters": "Recent supporters",
312
+ "share_copy_link": "Copy link",
313
+ "share_join_cta": "Susține și tu acest mural",
314
+ "share_pixel_cta": "Distribuie această susținere",
315
+ "share_pixel_default_desc": "See who is supporting the {wall} wall.",
316
+ "share_pixel_text": "{name} supported the {wall} wall. Join in and support too.",
317
+ "share_pixel_title": "{name} supported {wall}",
318
+ "share_via_facebook": "Share on Facebook",
319
+ "share_via_twitter": "Share on X",
320
+ "share_via_whatsapp": "Share on WhatsApp",
321
+ "sold": "sold",
322
+ "spots_unit": "pixeli",
323
+ "step_back": "Back",
324
+ "step_block_auto": "Choose automatically",
325
+ "step_block_confirm": "Confirm position",
326
+ "step_block_help": "Choose where your {count} pixels will appear on the wall.",
327
+ "step_block_manual": "Choose manually",
328
+ "step_block_quantity": "Quantity",
329
+ "step_block_shape": "Shape",
330
+ "step_block_title": "Choose your space",
331
+ "step_block_total": "{total} pixels",
332
+ "step_customize_continue": "Continue",
333
+ "step_customize_help": "Upload your image, title, and message to appear on the wall.",
334
+ "step_customize_image": "Support image",
335
+ "step_customize_image_hint": "Use a square image or crop it before continuing.",
336
+ "step_customize_link": "Optional link",
337
+ "step_customize_link_error": "Enter a valid link.",
338
+ "step_customize_remove_image": "Remove image",
339
+ "step_customize_replace_image": "Replace image",
340
+ "step_customize_title": "Customize your support",
341
+ "step_customize_upload_image": "Upload image",
342
+ "step_goal_help": "Choose a goal to direct your support.",
343
+ "step_goal_skip_desc": "Your support goes to the general wall without linking to a specific goal.",
344
+ "step_goal_skip_title": "Support without a goal",
345
+ "step_goal_title": "Choose a goal",
346
+ "step_package_custom_cta": "Set quantity",
347
+ "step_package_custom_desc": "Choose how many pixels you want to buy.",
348
+ "step_package_custom_title": "Custom",
349
+ "step_package_help": "Choose a pixel package for the wall.",
350
+ "step_package_title": "Choose your package",
351
+ "step_pay_method": "Payment method",
352
+ "step_pay_title": "Payment",
353
+ "step_pay_total": "Total",
354
+ "supporters": "Susținători",
355
+ "tiers": "Packages"
356
+ }
357
+ }
243
358
  }
244
359
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -281,7 +281,22 @@ async function onStepperSuccess() {
281
281
 
282
282
  function supporterDisplayName(s: any): string {
283
283
  if (s.is_anonymous) return t('tv.dashboard.indie_wall.anonymous')
284
- return s.title || s.user?.name || t('tv.dashboard.indie_wall.anonymous')
284
+ return s.user?.nickname
285
+ || s.user?.username
286
+ || s.user?.full_name
287
+ || s.user?.name
288
+ || s.guest_name
289
+ || t('tv.dashboard.indie_wall.anonymous')
290
+ }
291
+
292
+ function supporterAvatarUrl(s: any): string {
293
+ if (s.is_anonymous) return ''
294
+ return s.user?.avatar_url || s.user?.profile_image || s.user?.avatar || s.user?.image || ''
295
+ }
296
+
297
+ function supporterTitle(s: any): string {
298
+ if (!s?.title) return ''
299
+ return s.title !== supporterDisplayName(s) ? s.title : ''
285
300
  }
286
301
 
287
302
  const requestUrl = useRequestURL()
@@ -601,11 +616,11 @@ onUnmounted(() => {
601
616
  </div>
602
617
  <div class="supp-body">
603
618
  <div class="supp-identity">
604
- <img v-if="s.user?.avatar_url || s.user?.profile_image" :src="s.user.avatar_url || s.user.profile_image" class="supp-avi" alt="" />
619
+ <img v-if="supporterAvatarUrl(s)" :src="supporterAvatarUrl(s)" class="supp-avi" alt="" />
605
620
  <div v-else class="supp-avi-init">{{ (supporterDisplayName(s)[0] || '?').toUpperCase() }}</div>
606
621
  <div class="supp-user-info">
607
622
  <span class="supp-name">{{ supporterDisplayName(s) }}</span>
608
- <span v-if="!s.is_anonymous && !s.user && s.guest_email" class="supp-email">{{ s.guest_email }}</span>
623
+ <span v-if="supporterTitle(s)" class="supp-title">{{ supporterTitle(s) }}</span>
609
624
  </div>
610
625
  <span class="supp-amount">{{ formatMoney(Number(s.amount_paid || 0), wall.currency) }}</span>
611
626
  </div>
@@ -721,6 +736,7 @@ onUnmounted(() => {
721
736
  </div>
722
737
  <div class="detail-body">
723
738
  <p class="detail-name">{{ supporterDisplayName(detailPixel) }}</p>
739
+ <p v-if="supporterTitle(detailPixel)" class="detail-support-title">{{ supporterTitle(detailPixel) }}</p>
724
740
  <p v-if="detailPixel.supporter_message" class="detail-msg">"{{ detailPixel.supporter_message }}"</p>
725
741
  <div class="detail-amount-row">
726
742
  <span class="detail-amount-label">{{ $t('tv.dashboard.indie_wall.spots_unit') }}</span>
@@ -1006,7 +1022,7 @@ onUnmounted(() => {
1006
1022
  color: var(--title-fg, #fff); font-size: 0.78rem; font-weight: 500;
1007
1023
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1008
1024
  }
1009
- .supp-email {
1025
+ .supp-title {
1010
1026
  color: var(--secondary-info-fg, #666); font-size: 0.65rem;
1011
1027
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1012
1028
  }
@@ -1051,7 +1067,11 @@ onUnmounted(() => {
1051
1067
  }
1052
1068
  .detail-body { padding: 20px; }
1053
1069
  .detail-name {
1054
- font-size: 1.1rem; font-weight: 700; color: var(--title-fg, #fff); margin: 0 0 8px;
1070
+ font-size: 1.1rem; font-weight: 700; color: var(--title-fg, #fff); margin: 0 0 4px;
1071
+ }
1072
+ .detail-support-title {
1073
+ color: var(--secondary-info-fg, #aaa); font-size: 0.78rem;
1074
+ line-height: 1.35; margin: 0 0 10px;
1055
1075
  }
1056
1076
  .detail-msg {
1057
1077
  color: var(--secondary-info-fg, #aaa); font-size: 0.875rem; font-style: italic;
@@ -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>