@mundogamernetwork/shared-ui 1.8.22 → 1.9.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/locales/de.json CHANGED
@@ -22,13 +22,54 @@
22
22
  "no_requests": "Sie haben noch keine Keys angefordert"
23
23
  },
24
24
  "campaigns": {
25
+ "feedback": {
26
+ "title": "Dieses Spiel bewerten",
27
+ "subtitle": "Du hast einen Key für {game} erhalten — sag anderen Creators, was du davon hältst.",
28
+ "rating_label": "Deine Bewertung",
29
+ "title_placeholder": "In einem Satz zusammengefasst (optional)",
30
+ "body_placeholder": "Was funktioniert, was nicht, für wen es ist (optional)",
31
+ "disclosure": "Ich habe einen Key aus dieser Kampagne erhalten und diese Bewertung ist meine ehrliche Meinung.",
32
+ "average_note": "Deine Bewertung zählt für den öffentlichen Durchschnitt des Spiels und wird mit einem Hinweis angezeigt, dass du einen Key erhalten hast.",
33
+ "send": "Bewertung veröffentlichen",
34
+ "sending": "Wird veröffentlicht…",
35
+ "submitted": "Deine Bewertung ist jetzt auf der Seite von {game}.",
36
+ "view_on_game_page": "Dort ansehen"
37
+ },
38
+ "comments": {
39
+ "title": "Creator-Feedback",
40
+ "subtitle": "Eindrücke von Creators, die mit einem Key aus dieser Kampagne gespielt haben.",
41
+ "placeholder": "Erzähl, was du vom Spiel hältst…",
42
+ "send": "Veröffentlichen",
43
+ "sending": "Wird veröffentlicht…",
44
+ "empty": "Noch kein Feedback. Teile als Erste:r deine Eindrücke.",
45
+ "load_more": "Mehr laden",
46
+ "load_error": "Das Feedback konnte nicht geladen werden. Versuch es gleich noch einmal.",
47
+ "sign_in_notice": "Melde dich an, um mitzureden.",
48
+ "creators_only_notice": "Nur Creators, die einen Key aus dieser Kampagne erhalten haben, können hier posten.",
49
+ "reply": "Antworten",
50
+ "reply_placeholder": "Antwort schreiben…",
51
+ "edited": "bearbeitet",
52
+ "hide": "Ausblenden",
53
+ "unhide": "Wieder einblenden",
54
+ "hidden_badge": "Ausgeblendet",
55
+ "unknown_author": "Creator",
56
+ "save": "Speichern",
57
+ "cancel": "Abbrechen",
58
+ "edit": "Bearbeiten",
59
+ "delete": "Löschen"
60
+ },
25
61
  "title": "Key Campaigns",
26
62
  "my_requests": "My Requests",
63
+ "calendar_export_label": "Diese Fristen in deinen Kalender übernehmen:",
64
+ "calendar_export_ics": "Kalender (.ics)",
65
+ "calendar_export_csv": "Tabelle (.csv)",
66
+ "calendar_export_busy": "Wird vorbereitet...",
67
+ "calendar_export_error": "Die Datei konnte nicht erstellt werden. Bitte versuche es erneut.",
27
68
  "all": "All",
28
69
  "details": "Details",
29
70
  "see_campaign": "See campaign",
30
71
  "see_content": "See content",
31
- "redeem": "Redeem",
72
+ "redeem": "Request",
32
73
  "load_more": "Load more",
33
74
  "no_results": "No campaigns found",
34
75
  "days": "days",
@@ -67,7 +108,7 @@
67
108
  "title_span_0": "ended",
68
109
  "obs_0": "The redemption period for the key has expired.",
69
110
  "title_1": "Now you need to",
70
- "title_span_1": "redeem the key",
111
+ "title_span_1": "request the key",
71
112
  "obs_1": "Check the campaign requirements before requesting your key(s)",
72
113
  "title_2": "Now just",
73
114
  "title_span_2": "wait",
@@ -131,7 +172,7 @@
131
172
  "minutes": "min",
132
173
  "seconds": "sec",
133
174
  "days_hours": "days and hours",
134
- "redeem": "Redeem key",
175
+ "redeem": "Request key",
135
176
  "actions": "Complementary actions",
136
177
  "no_actions": "No actions configured",
137
178
  "requirements": "Requirements",
@@ -195,6 +236,7 @@
195
236
  "required_data": "Erforderliche Angaben",
196
237
  "already_requested": "Sie haben bereits eine Anfrage für diese Kampagne gestellt.",
197
238
  "sold_out": "Alle Keys für diese Kampagne wurden bereits eingelöst.",
239
+ "options_unavailable": "Diese Kampagne hat noch Keys, aber es ist noch keine Plattform oder Region dafür eingerichtet, daher können keine Anfragen gesendet werden. Das Studio muss die Einrichtung abschließen — auf deiner Seite ist alles in Ordnung.",
198
240
  "keys_left": "{n} Key übrig | {n} Keys übrig",
199
241
  "keys_low_stock": "Letzter Key | Letzte {n}",
200
242
  "back_to_campaign": "Zur Kampagne zurückkehren",
package/locales/en.json CHANGED
@@ -22,13 +22,54 @@
22
22
  "no_requests": "You haven't requested any keys yet"
23
23
  },
24
24
  "campaigns": {
25
+ "feedback": {
26
+ "title": "Rate this game",
27
+ "subtitle": "You received a key for {game} — tell other creators what you thought.",
28
+ "rating_label": "Your rating",
29
+ "title_placeholder": "Sum it up in a line (optional)",
30
+ "body_placeholder": "What worked, what didn't, who it's for (optional)",
31
+ "disclosure": "I received a key from this campaign and this review is my honest opinion.",
32
+ "average_note": "Your rating counts towards this game's public average and will be shown with a badge saying you received a key.",
33
+ "send": "Publish rating",
34
+ "sending": "Publishing…",
35
+ "submitted": "Your rating is live on {game}'s page.",
36
+ "view_on_game_page": "See it there"
37
+ },
38
+ "comments": {
39
+ "title": "Creator feedback",
40
+ "subtitle": "Impressions from creators who played with a key from this campaign.",
41
+ "placeholder": "Share what you thought of the game…",
42
+ "send": "Post",
43
+ "sending": "Posting…",
44
+ "empty": "No feedback yet. Be the first to share your impressions.",
45
+ "load_more": "Load more",
46
+ "load_error": "We couldn't load the feedback. Try again shortly.",
47
+ "sign_in_notice": "Sign in to join the conversation.",
48
+ "creators_only_notice": "Only creators who received a key from this campaign can post here.",
49
+ "reply": "Reply",
50
+ "reply_placeholder": "Write a reply…",
51
+ "edited": "edited",
52
+ "hide": "Hide",
53
+ "unhide": "Restore",
54
+ "hidden_badge": "Hidden",
55
+ "unknown_author": "Creator",
56
+ "save": "Save",
57
+ "cancel": "Cancel",
58
+ "edit": "Edit",
59
+ "delete": "Delete"
60
+ },
25
61
  "title": "Key Campaigns",
26
62
  "my_requests": "My Requests",
63
+ "calendar_export_label": "Add these deadlines to your calendar:",
64
+ "calendar_export_ics": "Calendar (.ics)",
65
+ "calendar_export_csv": "Spreadsheet (.csv)",
66
+ "calendar_export_busy": "Preparing...",
67
+ "calendar_export_error": "Could not generate the file. Please try again.",
27
68
  "all": "All",
28
69
  "details": "Details",
29
70
  "see_campaign": "See campaign",
30
71
  "see_content": "See content",
31
- "redeem": "Redeem",
72
+ "redeem": "Request",
32
73
  "load_more": "Load more",
33
74
  "no_results": "No campaigns found",
34
75
  "days": "days",
@@ -67,7 +108,7 @@
67
108
  "title_span_0": "ended",
68
109
  "obs_0": "The redemption period for the key has expired.",
69
110
  "title_1": "Now you need to",
70
- "title_span_1": "redeem the key",
111
+ "title_span_1": "request the key",
71
112
  "obs_1": "Check the campaign requirements before requesting your key(s)",
72
113
  "title_2": "Now just",
73
114
  "title_span_2": "wait",
@@ -131,7 +172,7 @@
131
172
  "minutes": "min",
132
173
  "seconds": "sec",
133
174
  "days_hours": "days and hours",
134
- "redeem": "Redeem key",
175
+ "redeem": "Request key",
135
176
  "actions": "Complementary actions",
136
177
  "no_actions": "No actions configured",
137
178
  "requirements": "Requirements",
@@ -195,6 +236,7 @@
195
236
  "required_data": "Required data",
196
237
  "already_requested": "You have already submitted a request for this campaign.",
197
238
  "sold_out": "All keys for this campaign have already been claimed.",
239
+ "options_unavailable": "This campaign still has keys, but no platform or region is set up for them yet, so requests can't be sent. The studio needs to finish setting it up — nothing on your side is wrong.",
198
240
  "keys_left": "{n} key left | {n} keys left",
199
241
  "keys_low_stock": "Last key | Last {n}",
200
242
  "back_to_campaign": "Back to campaign",
package/locales/es.json CHANGED
@@ -22,13 +22,54 @@
22
22
  "no_requests": "Aún no has solicitado ninguna clave."
23
23
  },
24
24
  "campaigns": {
25
+ "feedback": {
26
+ "title": "Valora este juego",
27
+ "subtitle": "Recibiste una key de {game}: cuéntales a otros creadores qué te pareció.",
28
+ "rating_label": "Tu valoración",
29
+ "title_placeholder": "Resúmelo en una línea (opcional)",
30
+ "body_placeholder": "Qué funcionó, qué no, para quién es (opcional)",
31
+ "disclosure": "Recibí una key de esta campaña y esta reseña es mi opinión honesta.",
32
+ "average_note": "Tu valoración cuenta para la media pública del juego y se mostrará con una insignia que indica que recibiste una key.",
33
+ "send": "Publicar valoración",
34
+ "sending": "Publicando…",
35
+ "submitted": "Tu valoración ya está en la página de {game}.",
36
+ "view_on_game_page": "Verla allí"
37
+ },
38
+ "comments": {
39
+ "title": "Opiniones de creadores",
40
+ "subtitle": "Impresiones de creadores que jugaron con una key de esta campaña.",
41
+ "placeholder": "Cuenta qué te pareció el juego…",
42
+ "send": "Publicar",
43
+ "sending": "Publicando…",
44
+ "empty": "Todavía no hay opiniones. Sé el primero en compartir las tuyas.",
45
+ "load_more": "Cargar más",
46
+ "load_error": "No pudimos cargar las opiniones. Inténtalo de nuevo en un momento.",
47
+ "sign_in_notice": "Inicia sesión para unirte a la conversación.",
48
+ "creators_only_notice": "Solo los creadores que recibieron una key de esta campaña pueden publicar aquí.",
49
+ "reply": "Responder",
50
+ "reply_placeholder": "Escribe una respuesta…",
51
+ "edited": "editado",
52
+ "hide": "Ocultar",
53
+ "unhide": "Restaurar",
54
+ "hidden_badge": "Oculto",
55
+ "unknown_author": "Creador",
56
+ "save": "Guardar",
57
+ "cancel": "Cancelar",
58
+ "edit": "Editar",
59
+ "delete": "Eliminar"
60
+ },
25
61
  "title": "Campañas de Claves",
26
62
  "my_requests": "Mis Solicitudes",
63
+ "calendar_export_label": "Lleva estos plazos a tu calendario:",
64
+ "calendar_export_ics": "Calendario (.ics)",
65
+ "calendar_export_csv": "Hoja de cálculo (.csv)",
66
+ "calendar_export_busy": "Preparando...",
67
+ "calendar_export_error": "No se pudo generar el archivo. Inténtalo de nuevo.",
27
68
  "all": "Todas",
28
69
  "details": "Detalles",
29
70
  "see_campaign": "Ver Campaña",
30
71
  "see_content": "Ver Contenido",
31
- "redeem": "Canjear",
72
+ "redeem": "Solicitar",
32
73
  "load_more": "Cargar Más",
33
74
  "no_results": "No se encontraron resultados",
34
75
  "days": "días",
@@ -67,7 +108,7 @@
67
108
  "title_span_0": "finalizada",
68
109
  "obs_0": "El período de canje de la clave ha expirado.",
69
110
  "title_1": "Ahora necesitas",
70
- "title_span_1": "canjear la clave",
111
+ "title_span_1": "solicitar la clave",
71
112
  "obs_1": "Revisa los requisitos de la campaña antes de solicitar tu(s) clave(s)",
72
113
  "title_2": "Ahora solo",
73
114
  "title_span_2": "espera",
@@ -131,7 +172,7 @@
131
172
  "minutes": "minutos",
132
173
  "seconds": "segundos",
133
174
  "days_hours": "Fechas y horarios",
134
- "redeem": "Canjear Clave",
175
+ "redeem": "Solicitar Clave",
135
176
  "actions": "Acciones",
136
177
  "no_actions": "No hay acciones adicionales definidas para esta campaña.",
137
178
  "requirements": "Requisitos",
@@ -195,6 +236,7 @@
195
236
  "required_data": "Datos requeridos",
196
237
  "already_requested": "Ya has enviado una solicitud para esta campaña.",
197
238
  "sold_out": "Todas las claves de esta campaña ya fueron reclamadas.",
239
+ "options_unavailable": "Esta campaña todavía tiene claves, pero aún no hay ninguna plataforma o región configurada para ellas, así que no se pueden enviar solicitudes. El estudio debe terminar la configuración — no hay nada mal de tu lado.",
198
240
  "keys_left": "{n} clave restante | {n} claves restantes",
199
241
  "keys_low_stock": "Última clave | Últimas {n}",
200
242
  "back_to_campaign": "Volver a la campaña",
@@ -22,13 +22,54 @@
22
22
  "no_requests": "Você ainda não solicitou nenhuma chave"
23
23
  },
24
24
  "campaigns": {
25
+ "feedback": {
26
+ "title": "Avalie este jogo",
27
+ "subtitle": "Você recebeu uma key de {game} — conte para outros criadores o que achou.",
28
+ "rating_label": "Sua nota",
29
+ "title_placeholder": "Resuma em uma linha (opcional)",
30
+ "body_placeholder": "O que funcionou, o que não funcionou, para quem é (opcional)",
31
+ "disclosure": "Recebi uma key desta campanha e esta avaliação é a minha opinião honesta.",
32
+ "average_note": "Sua nota entra na média pública do jogo e aparece com um selo informando que você recebeu uma key.",
33
+ "send": "Publicar avaliação",
34
+ "sending": "Publicando…",
35
+ "submitted": "Sua avaliação já está na página de {game}.",
36
+ "view_on_game_page": "Ver lá"
37
+ },
38
+ "comments": {
39
+ "title": "Feedback dos criadores",
40
+ "subtitle": "Impressões de criadores que jogaram com uma key desta campanha.",
41
+ "placeholder": "Conte o que achou do jogo…",
42
+ "send": "Publicar",
43
+ "sending": "Publicando…",
44
+ "empty": "Ainda não há feedback. Seja o primeiro a contar suas impressões.",
45
+ "load_more": "Carregar mais",
46
+ "load_error": "Não conseguimos carregar o feedback. Tente novamente em instantes.",
47
+ "sign_in_notice": "Entre na sua conta para participar da conversa.",
48
+ "creators_only_notice": "Apenas criadores que receberam uma key desta campanha podem publicar aqui.",
49
+ "reply": "Responder",
50
+ "reply_placeholder": "Escreva uma resposta…",
51
+ "edited": "editado",
52
+ "hide": "Ocultar",
53
+ "unhide": "Reexibir",
54
+ "hidden_badge": "Oculto",
55
+ "unknown_author": "Criador",
56
+ "save": "Salvar",
57
+ "cancel": "Cancelar",
58
+ "edit": "Editar",
59
+ "delete": "Excluir"
60
+ },
25
61
  "title": "Key Campaigns",
26
62
  "my_requests": "My Requests",
63
+ "calendar_export_label": "Leve estes prazos para o seu calendário:",
64
+ "calendar_export_ics": "Calendário (.ics)",
65
+ "calendar_export_csv": "Planilha (.csv)",
66
+ "calendar_export_busy": "Preparando...",
67
+ "calendar_export_error": "Não foi possível gerar o arquivo. Tente novamente.",
27
68
  "all": "All",
28
69
  "details": "Details",
29
70
  "see_campaign": "See campaign",
30
71
  "see_content": "See content",
31
- "redeem": "Redeem",
72
+ "redeem": "Request",
32
73
  "load_more": "Load more",
33
74
  "no_results": "No campaigns found",
34
75
  "days": "days",
@@ -67,7 +108,7 @@
67
108
  "title_span_0": "ended",
68
109
  "obs_0": "The redemption period for the key has expired.",
69
110
  "title_1": "Now you need to",
70
- "title_span_1": "redeem the key",
111
+ "title_span_1": "request the key",
71
112
  "obs_1": "Check the campaign requirements before requesting your key(s)",
72
113
  "title_2": "Now just",
73
114
  "title_span_2": "wait",
@@ -131,7 +172,7 @@
131
172
  "minutes": "min",
132
173
  "seconds": "sec",
133
174
  "days_hours": "days and hours",
134
- "redeem": "Redeem key",
175
+ "redeem": "Request key",
135
176
  "actions": "Complementary actions",
136
177
  "no_actions": "No actions configured",
137
178
  "requirements": "Requirements",
@@ -195,6 +236,7 @@
195
236
  "required_data": "Dados obrigatórios",
196
237
  "already_requested": "Você já enviou uma solicitação para esta campanha.",
197
238
  "sold_out": "Todas as chaves desta campanha já foram resgatadas.",
239
+ "options_unavailable": "Esta campanha ainda tem chaves, mas nenhuma plataforma ou região foi configurada para elas, então não dá para enviar pedidos. O estúdio precisa concluir a configuração — não há nada errado do seu lado.",
198
240
  "keys_left": "{n} chave restante | {n} chaves restantes",
199
241
  "keys_low_stock": "Última chave | Últimas {n}",
200
242
  "back_to_campaign": "Voltar para a campanha",
package/locales/ro.json CHANGED
@@ -22,13 +22,54 @@
22
22
  "no_requests": "Nu ai solicitat nicio cheie încă"
23
23
  },
24
24
  "campaigns": {
25
+ "feedback": {
26
+ "title": "Evaluează acest joc",
27
+ "subtitle": "Ai primit un key pentru {game} — spune-le altor creatori ce ai crezut.",
28
+ "rating_label": "Nota ta",
29
+ "title_placeholder": "Rezumă într-o linie (opțional)",
30
+ "body_placeholder": "Ce a mers, ce nu, pentru cine e (opțional)",
31
+ "disclosure": "Am primit un key din această campanie, iar această recenzie este opinia mea sinceră.",
32
+ "average_note": "Nota ta contează pentru media publică a jocului și va fi afișată cu o insignă care arată că ai primit un key.",
33
+ "send": "Publică evaluarea",
34
+ "sending": "Se publică…",
35
+ "submitted": "Evaluarea ta este pe pagina jocului {game}.",
36
+ "view_on_game_page": "Vezi acolo"
37
+ },
38
+ "comments": {
39
+ "title": "Feedback de la creatori",
40
+ "subtitle": "Impresii ale creatorilor care au jucat cu un key din această campanie.",
41
+ "placeholder": "Spune ce ai crezut despre joc…",
42
+ "send": "Publică",
43
+ "sending": "Se publică…",
44
+ "empty": "Încă nu există feedback. Fii primul care își împărtășește impresiile.",
45
+ "load_more": "Încarcă mai multe",
46
+ "load_error": "Nu am putut încărca feedbackul. Încearcă din nou în scurt timp.",
47
+ "sign_in_notice": "Autentifică-te ca să participi la conversație.",
48
+ "creators_only_notice": "Doar creatorii care au primit un key din această campanie pot publica aici.",
49
+ "reply": "Răspunde",
50
+ "reply_placeholder": "Scrie un răspuns…",
51
+ "edited": "editat",
52
+ "hide": "Ascunde",
53
+ "unhide": "Reafișează",
54
+ "hidden_badge": "Ascuns",
55
+ "unknown_author": "Creator",
56
+ "save": "Salvează",
57
+ "cancel": "Anulează",
58
+ "edit": "Editează",
59
+ "delete": "Șterge"
60
+ },
25
61
  "title": "Key Campaigns",
26
62
  "my_requests": "My Requests",
63
+ "calendar_export_label": "Adaugă aceste termene în calendarul tău:",
64
+ "calendar_export_ics": "Calendar (.ics)",
65
+ "calendar_export_csv": "Foaie de calcul (.csv)",
66
+ "calendar_export_busy": "Se pregătește...",
67
+ "calendar_export_error": "Fișierul nu a putut fi generat. Încearcă din nou.",
27
68
  "all": "All",
28
69
  "details": "Details",
29
70
  "see_campaign": "See campaign",
30
71
  "see_content": "See content",
31
- "redeem": "Redeem",
72
+ "redeem": "Request",
32
73
  "load_more": "Load more",
33
74
  "no_results": "No campaigns found",
34
75
  "days": "days",
@@ -67,7 +108,7 @@
67
108
  "title_span_0": "ended",
68
109
  "obs_0": "The redemption period for the key has expired.",
69
110
  "title_1": "Now you need to",
70
- "title_span_1": "redeem the key",
111
+ "title_span_1": "request the key",
71
112
  "obs_1": "Check the campaign requirements before requesting your key(s)",
72
113
  "title_2": "Now just",
73
114
  "title_span_2": "wait",
@@ -131,7 +172,7 @@
131
172
  "minutes": "min",
132
173
  "seconds": "sec",
133
174
  "days_hours": "days and hours",
134
- "redeem": "Redeem key",
175
+ "redeem": "Request key",
135
176
  "actions": "Complementary actions",
136
177
  "no_actions": "No actions configured",
137
178
  "requirements": "Requirements",
@@ -195,6 +236,7 @@
195
236
  "required_data": "Date obligatorii",
196
237
  "already_requested": "Ați trimis deja o cerere pentru această campanie.",
197
238
  "sold_out": "Toate cheile pentru această campanie au fost deja revendicate.",
239
+ "options_unavailable": "Această campanie mai are chei, dar încă nu este configurată nicio platformă sau regiune pentru ele, așa că nu se pot trimite cereri. Studioul trebuie să finalizeze configurarea — nu este nimic greșit din partea ta.",
198
240
  "keys_left": "{n} cheie rămasă | {n} chei rămase",
199
241
  "keys_low_stock": "Ultima cheie | Ultimele {n}",
200
242
  "back_to_campaign": "Înapoi la campanie",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.8.22",
3
+ "version": "1.9.0",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -1,5 +1,5 @@
1
1
  <script lang="ts" setup>
2
- import { ref, computed, inject, onMounted, unref } from "vue"
2
+ import { ref, computed, inject, onMounted, onUnmounted, watch, unref } from "vue"
3
3
  import { useNuxtApp, useRoute, useAsyncData, navigateTo } from "#app"
4
4
  import {
5
5
  fetchCampaignBySlug,
@@ -235,55 +235,86 @@ async function toggleFollow() {
235
235
 
236
236
  let countdownInterval: ReturnType<typeof setInterval> | null = null
237
237
 
238
- const updateCountdown = () => {
238
+ const stopCountdown = () => {
239
+ if (countdownInterval) {
240
+ clearInterval(countdownInterval)
241
+ countdownInterval = null
242
+ }
243
+ }
244
+
245
+ const setTimeValues = (totalSeconds: number) => {
246
+ const remaining = Math.max(0, totalSeconds)
247
+ timeValues.value = {
248
+ days: Math.floor(remaining / 86400),
249
+ hours: Math.floor((remaining % 86400) / 3600),
250
+ minutes: Math.floor((remaining % 3600) / 60),
251
+ seconds: remaining % 60,
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Seconds until whichever deadline this campaign is currently counting to, or
257
+ * null when there is nothing to count down (no end date, or data not in yet).
258
+ *
259
+ * Returning null rather than a placeholder number matters: the previous code
260
+ * parked 999 days in the counter for the no-deadline case, which is
261
+ * indistinguishable from a real value once it is on screen — and because the
262
+ * counter was computed exactly once, a campaign whose data landed after mount
263
+ * kept that 999 forever. It also fed the status badge (`timeValues.days > 7`),
264
+ * so a campaign ending in three days rendered as "active" instead of "ending".
265
+ */
266
+ const secondsRemaining = (): number | null => {
239
267
  if (campaignNotYetOpen.value) {
240
268
  // Count down to the real opening date instead of end_date — there is no
241
269
  // server-computed "time to start" field, so this is derived client-side.
242
- let totalSeconds = Math.max(0, Math.floor((new Date(campaign.value!.start!).getTime() - Date.now()) / 1000))
243
- countdownInterval = setInterval(() => {
244
- if (totalSeconds <= 0) {
245
- if (countdownInterval) clearInterval(countdownInterval)
246
- return
247
- }
248
- totalSeconds--
249
- timeValues.value = {
250
- days: Math.floor(totalSeconds / 86400),
251
- hours: Math.floor((totalSeconds % 86400) / 3600),
252
- minutes: Math.floor((totalSeconds % 3600) / 60),
253
- seconds: totalSeconds % 60,
254
- }
255
- }, 1000)
256
- return
270
+ return Math.max(0, Math.floor((new Date(campaign.value!.start!).getTime() - Date.now()) / 1000))
257
271
  }
258
- if (hasNoDateLimit.value) {
259
- timeValues.value = { days: 999, hours: 0, minutes: 0, seconds: 0 }
272
+ if (hasNoDateLimit.value) return null
273
+
274
+ const raw = campaign.value?.time_to_expire
275
+ if (!raw) return null
276
+
277
+ const [d, h, m, s] = raw.split(":").map(Number)
278
+ if ([d, h, m, s].some((part) => !Number.isFinite(part))) return null
279
+
280
+ return d * 86400 + h * 3600 + m * 60 + s
281
+ }
282
+
283
+ const updateCountdown = () => {
284
+ // Always restart from scratch: this runs again whenever the campaign ref
285
+ // changes, and two live intervals would double the tick rate.
286
+ stopCountdown()
287
+
288
+ const total = secondsRemaining()
289
+ if (total === null) {
290
+ timeValues.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
260
291
  return
261
292
  }
262
- if (!campaign.value?.time_to_expire) return
263
293
 
264
- const [d, h, m, s] = campaign.value.time_to_expire.split(":").map(Number)
265
- let totalSeconds = d * 86400 + h * 3600 + m * 60 + s
294
+ // Paint before the first tick, otherwise the counter sits at 0 for a full
295
+ // second on every page load.
296
+ let remaining = total
297
+ setTimeValues(remaining)
266
298
 
267
299
  countdownInterval = setInterval(() => {
268
- if (totalSeconds <= 0) {
269
- if (countdownInterval) clearInterval(countdownInterval)
270
- return
271
- }
272
- totalSeconds--
273
- timeValues.value = {
274
- days: Math.floor(totalSeconds / 86400),
275
- hours: Math.floor((totalSeconds % 86400) / 3600),
276
- minutes: Math.floor((totalSeconds % 3600) / 60),
277
- seconds: totalSeconds % 60,
278
- }
300
+ remaining--
301
+ setTimeValues(remaining)
302
+ if (remaining <= 0) stopCountdown()
279
303
  }, 1000)
280
304
  }
281
305
 
306
+ // The campaign can hydrate after this component mounts, and the counter used to
307
+ // be computed a single time in onMounted — whatever it read at that instant
308
+ // (usually nothing) was what stayed on screen. onMounted covers the data-ready
309
+ // case and keeps setInterval off the server; the watcher covers the late one.
310
+ watch(campaign, updateCountdown)
311
+ onUnmounted(stopCountdown)
312
+
282
313
  // Embargo is shown with day AND time in the timezone the campaign creator set
283
314
  // it in — formatDate() below renders in the viewer's own zone, which turns one
284
315
  // deadline into a different one for every country.
285
316
  const embargoDateTime = computed(() =>
286
- formatEmbargoDateTime(campaign.value?.embargo_date, campaign.value?.embargo_timezone, locale.value),
317
+ formatEmbargoDateTime(campaign.value?.embargo_display, locale.value),
287
318
  )
288
319
 
289
320
  const formatDate = (dateString: string | null | undefined): string => {
@@ -1041,6 +1072,27 @@ onMounted(async () => {
1041
1072
  </div>
1042
1073
  </div>
1043
1074
  </div>
1075
+ <!-- Creator feedback. Both blocks self-hide when the viewer has no
1076
+ business seeing them: the rating form needs a revealed key, and the
1077
+ comment wall renders read-only for everyone else. Permission flags
1078
+ come from the campaign payload so the UI never contradicts the API. -->
1079
+ <div v-if="campaign?.id" class="container">
1080
+ <KeyCampaignFeedback
1081
+ :key-id="campaign.id"
1082
+ :can-rate="campaign.can_rate"
1083
+ :game-name="campaign.game_name"
1084
+ :game-slug="campaign.game_slug"
1085
+ :locale="locale"
1086
+ />
1087
+ <KeyCampaignComments
1088
+ v-if="campaign.allow_comments !== false"
1089
+ :key-id="campaign.id"
1090
+ :can-comment="campaign.can_comment"
1091
+ :can-moderate="campaign.can_moderate_comments"
1092
+ :signed-in="!!currentUser"
1093
+ />
1094
+ </div>
1095
+
1044
1096
  <KeyCampaignsCarousel
1045
1097
  :exclude-id="campaign?.id ?? null"
1046
1098
  :mg-platform="mgPlatform"