@mundogamernetwork/shared-ui 1.4.2 → 1.5.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.
@@ -0,0 +1,195 @@
1
+ <script setup lang="ts">
2
+ // Tester-facing capture step for the "Video Playtest" category — record,
3
+ // upload, optionally drop timestamped markers, then submit. Mounted by
4
+ // PlaytestResponseFlow.vue as the video_playtest category branch.
5
+ import { ref, computed } from "vue";
6
+ import { usePlaytestVideoRecording } from "../../../composables/usePlaytestVideoRecording";
7
+
8
+ const props = defineProps<{
9
+ campaignId: number;
10
+ }>();
11
+
12
+ const emit = defineEmits<{
13
+ (e: "submitted"): void;
14
+ }>();
15
+
16
+ const rec = usePlaytestVideoRecording(props.campaignId);
17
+
18
+ const markerDraft = ref("");
19
+ const step = computed<"record" | "review" | "upload" | "done">(() => {
20
+ if (rec.recordedBlob.value && !rec.recordingId.value) return "review";
21
+ if (rec.recordingId.value) return "upload";
22
+ return "record";
23
+ });
24
+
25
+ function addMarker() {
26
+ if (!markerDraft.value.trim()) return;
27
+ rec.markNow(markerDraft.value.trim());
28
+ markerDraft.value = "";
29
+ }
30
+
31
+ async function submit() {
32
+ const result = await rec.submitVideoResponse();
33
+ if (result) emit("submitted");
34
+ }
35
+
36
+ function formatSeconds(s: number): string {
37
+ const m = Math.floor(s / 60);
38
+ const sec = s % 60;
39
+ return `${m}:${sec.toString().padStart(2, "0")}`;
40
+ }
41
+ </script>
42
+
43
+ <template>
44
+ <div class="video-capture">
45
+ <p v-if="rec.genericError.value" class="video-capture__error">{{ rec.genericError.value }}</p>
46
+
47
+ <!-- Record -->
48
+ <template v-if="step === 'record'">
49
+ <p class="video-capture__hint">
50
+ {{ $t("playtest.video_capture.hint", "Record your screen while you play. You can drop timestamped notes as you go.") }}
51
+ </p>
52
+
53
+ <div v-if="!rec.capturing.value" class="video-capture__actions">
54
+ <button type="button" class="btn primary" @click="rec.startCapture">
55
+ {{ $t("playtest.video_capture.start", "Start recording") }}
56
+ </button>
57
+ </div>
58
+
59
+ <div v-else class="video-capture__recording">
60
+ <span class="video-capture__rec-dot" />
61
+ {{ $t("playtest.video_capture.recording", "Recording…") }}
62
+
63
+ <div class="video-capture__marker-form">
64
+ <input
65
+ v-model="markerDraft"
66
+ type="text"
67
+ :placeholder="$t('playtest.video_capture.marker_placeholder', 'Note something at this moment…')"
68
+ @keyup.enter="addMarker"
69
+ />
70
+ <button type="button" class="btn" @click="addMarker">
71
+ {{ $t("playtest.video_capture.mark", "Mark") }}
72
+ </button>
73
+ </div>
74
+
75
+ <ul v-if="rec.markers.value.length" class="video-capture__marker-list">
76
+ <li v-for="(m, i) in rec.markers.value" :key="i">
77
+ <strong>{{ formatSeconds(m.timestamp_seconds) }}</strong> — {{ m.comment }}
78
+ </li>
79
+ </ul>
80
+
81
+ <button type="button" class="btn danger" @click="rec.stopCapture">
82
+ {{ $t("playtest.video_capture.stop", "Stop recording") }}
83
+ </button>
84
+ </div>
85
+ </template>
86
+
87
+ <!-- Review before upload -->
88
+ <template v-else-if="step === 'review'">
89
+ <p class="video-capture__hint">
90
+ {{ $t("playtest.video_capture.review_hint", "Recording captured ({v}). Upload it to submit your response.", { v: formatSeconds(rec.recordedDurationSeconds.value) }) }}
91
+ </p>
92
+ <button type="button" class="btn primary" :disabled="rec.uploading.value" @click="rec.uploadRecording">
93
+ {{ rec.uploading.value
94
+ ? $t("playtest.video_capture.uploading", "Uploading… {v}%", { v: rec.uploadProgress.value })
95
+ : $t("playtest.video_capture.upload", "Upload recording") }}
96
+ </button>
97
+ </template>
98
+
99
+ <!-- Upload done, waiting on provider processing — submit -->
100
+ <template v-else-if="step === 'upload'">
101
+ <p class="video-capture__hint">
102
+ {{ $t("playtest.video_capture.processing_hint", "Your recording is uploaded and processing. Submit when you're ready — we'll finish confirming it's ready in the background.") }}
103
+ </p>
104
+ <button type="button" class="btn primary" :disabled="rec.submitting.value" @click="submit">
105
+ {{ rec.submitting.value
106
+ ? $t("playtest.video_capture.submitting", "Submitting…")
107
+ : $t("playtest.video_capture.submit", "Submit response") }}
108
+ </button>
109
+ </template>
110
+ </div>
111
+ </template>
112
+
113
+ <style scoped lang="scss">
114
+ .video-capture {
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: 12px;
118
+
119
+ &__hint {
120
+ font-size: 13px;
121
+ color: var(--secondary-info-fg, #aaa);
122
+ margin: 0;
123
+ }
124
+
125
+ &__error {
126
+ font-size: 12px;
127
+ color: var(--danger, #e53935);
128
+ margin: 0;
129
+ }
130
+
131
+ &__actions {
132
+ display: flex;
133
+ }
134
+
135
+ &__recording {
136
+ display: flex;
137
+ flex-direction: column;
138
+ gap: 10px;
139
+ align-items: flex-start;
140
+ font-size: 13px;
141
+ color: var(--card-article-title, #fff);
142
+ }
143
+
144
+ &__rec-dot {
145
+ display: inline-block;
146
+ width: 8px;
147
+ height: 8px;
148
+ border-radius: 50%;
149
+ background: var(--danger, #e53935);
150
+ margin-right: 6px;
151
+ animation: video-capture-pulse 1.2s ease-in-out infinite;
152
+ }
153
+
154
+ &__marker-form {
155
+ display: flex;
156
+ gap: 8px;
157
+ width: 100%;
158
+
159
+ input {
160
+ flex: 1;
161
+ height: 40px;
162
+ padding: 0 10px;
163
+ background: transparent;
164
+ border: 1px solid var(--bg-app-badge);
165
+ color: var(--card-article-title, #fff);
166
+ font-size: 13px;
167
+ }
168
+ }
169
+
170
+ &__marker-list {
171
+ list-style: none;
172
+ margin: 0;
173
+ padding: 0;
174
+ font-size: 12px;
175
+ color: var(--secondary-info-fg, #aaa);
176
+ display: flex;
177
+ flex-direction: column;
178
+ gap: 4px;
179
+
180
+ strong {
181
+ color: var(--primary, #d297ff);
182
+ }
183
+ }
184
+ }
185
+
186
+ .btn.danger {
187
+ background: var(--danger, #e53935);
188
+ color: #fff;
189
+ }
190
+
191
+ @keyframes video-capture-pulse {
192
+ 0%, 100% { opacity: 1; }
193
+ 50% { opacity: 0.3; }
194
+ }
195
+ </style>
@@ -119,6 +119,26 @@
119
119
  />
120
120
  </svg>
121
121
  </div>
122
+ <div v-if="method === 4">
123
+ <svg width="52" height="18" viewBox="0 0 52 18" fill="none" xmlns="http://www.w3.org/2000/svg">
124
+ <path
125
+ d="M9.5 2.2L11.8 4.5C12.5 5.2 12.5 6.3 11.8 7L9.5 9.3C8.8 10 7.7 10 7 9.3L4.7 7C4 6.3 4 5.2 4.7 4.5L7 2.2C7.7 1.5 8.8 1.5 9.5 2.2Z"
126
+ :fill="theme === 'dark' ? 'white' : '#32BCAD'"
127
+ />
128
+ <path
129
+ d="M9.5 8.7L11.8 11C12.5 11.7 12.5 12.8 11.8 13.5L9.5 15.8C8.8 16.5 7.7 16.5 7 15.8L4.7 13.5C4 12.8 4 11.7 4.7 11L7 8.7C7.7 8 8.8 8 9.5 8.7Z"
130
+ :fill="theme === 'dark' ? 'white' : '#32BCAD'"
131
+ />
132
+ <text
133
+ x="17"
134
+ y="13.5"
135
+ font-family="Arial, Helvetica, sans-serif"
136
+ font-size="13"
137
+ font-weight="700"
138
+ :fill="theme === 'dark' ? 'white' : '#111827'"
139
+ >Pix</text>
140
+ </svg>
141
+ </div>
122
142
  </template>
123
143
  <script setup lang="ts">
124
144
  const props = defineProps({
@@ -9,40 +9,71 @@ export interface PaymentMethod {
9
9
  /**
10
10
  * Payment gateways shown in the checkout UI.
11
11
  *
12
- * These are FIXED to PayPal + Stripe the only gateways live across the
13
- * ecosystem. We do NOT read the `payment_methods` table: that table holds
14
- * generic legacy methods (Cash, Credit Card, Transfer, Bitcoin...) and has no
15
- * notion of PayPal/Stripe.
12
+ * These are FIXED across the ecosystemwe do NOT read the
13
+ * `payment_methods` table: that table holds generic legacy methods (Cash,
14
+ * Credit Card, Transfer, Bitcoin...) and has no notion of PayPal/Stripe/Pix.
16
15
  *
17
16
  * The actual gateway *account* (which api_credential to charge) is resolved by
18
17
  * the backend at checkout time, honouring the existing active / default /
19
18
  * production flags on `api_credentials`. That lets admins switch accounts
20
19
  * without any frontend change. The frontend only sends
21
- * `payment_gateway: 'paypal' | 'stripe'`.
20
+ * `payment_gateway: 'paypal' | 'stripe' | 'pix'`.
22
21
  *
23
- * The numeric `id` here (PayPal = 2, Stripe = 3) is the convention used by
24
- * <MgPaymentMethods> to pick which inline SVG logo to render. It is unrelated
25
- * to the payment_methods table ids and to api ids (PayPal = 4, Stripe = 5).
22
+ * The numeric `id` here (PayPal = 2, Stripe = 3, Pix = 4) is the convention
23
+ * used by <MgPaymentMethods> to pick which inline SVG logo to render. It is
24
+ * unrelated to the payment_methods table ids and to api ids.
26
25
  */
27
- const GATEWAYS: PaymentMethod[] = [
26
+ const ALL_GATEWAYS: PaymentMethod[] = [
28
27
  { id: 2, name: "paypal" },
29
28
  { id: 3, name: "stripe" },
29
+ { id: 4, name: "pix" },
30
30
  ];
31
31
 
32
+ /**
33
+ * Emergency/operational kill-switch: pulling a gateway from every checkout
34
+ * across the whole ecosystem is a name added here (ecosystem-wide default,
35
+ * requires bump + republish) or via a consuming app's
36
+ * NUXT_PUBLIC_DISABLED_PAYMENT_GATEWAYS runtime config (e.g. a provider
37
+ * outage), no per-app code change needed either way.
38
+ */
39
+ const DISABLED_BY_DEFAULT: string[] = [];
40
+
41
+ function disabledGatewayNames(): string[] {
42
+ let fromRuntimeConfig: string[] = [];
43
+ try {
44
+ // useRuntimeConfig is auto-imported by Nuxt; guarded for non-Nuxt/test contexts.
45
+ const raw = useRuntimeConfig?.()?.public?.disabledPaymentGateways;
46
+ if (Array.isArray(raw)) {
47
+ fromRuntimeConfig = raw;
48
+ } else if (typeof raw === "string" && raw.length) {
49
+ fromRuntimeConfig = raw.split(",").map((n) => n.trim());
50
+ }
51
+ } catch {
52
+ // Not inside a Nuxt context (e.g. unit test) — fall back to the default list.
53
+ }
54
+
55
+ return [...DISABLED_BY_DEFAULT, ...fromRuntimeConfig].map((n) => n.toLowerCase());
56
+ }
57
+
58
+ function enabledGateways(): PaymentMethod[] {
59
+ const disabled = disabledGatewayNames();
60
+ return ALL_GATEWAYS.filter((g) => !disabled.includes(g.name));
61
+ }
62
+
32
63
  export function usePaymentMethods(_httpService?: AxiosInstance) {
33
- const methods = ref<PaymentMethod[]>([...GATEWAYS]);
64
+ const methods = ref<PaymentMethod[]>(enabledGateways());
34
65
  const selectedMethod = ref<string>("");
35
66
  const loading = ref(false);
36
67
  const error = ref<string | null>(null);
37
68
 
38
69
  // No HTTP call. Kept async + the context arg for call-site compatibility.
39
70
  async function fetchMethods(_context: "checkout" | "subscription" = "checkout") {
40
- methods.value = [...GATEWAYS];
71
+ methods.value = enabledGateways();
41
72
  }
42
73
 
43
74
  const isStripe = computed(() => selectedMethod.value.toLowerCase() === "stripe");
44
75
  const isPaypal = computed(() => selectedMethod.value.toLowerCase() === "paypal");
45
- const isPix = computed(() => false); // Pix not enabled yet
76
+ const isPix = computed(() => selectedMethod.value.toLowerCase() === "pix");
46
77
 
47
78
  function selectMethod(name: string) {
48
79
  selectedMethod.value = name.toLowerCase();
@@ -0,0 +1,88 @@
1
+ import { ref } from "vue";
2
+ import {
3
+ fetchAccessGrant,
4
+ initiateAccessGrantWaiver,
5
+ verifyAccessGrantWaiver,
6
+ type PlaytestAccessGrant,
7
+ } from "../services/playtestTesterService";
8
+
9
+ /**
10
+ * Game-access grant state (key/link + liability waiver) for a single
11
+ * accepted response. Only meaningful once the response's status is
12
+ * 'accepted' — a submitted/rejected response has no grant to load.
13
+ * One instance per response — instantiate fresh per mount, mirroring
14
+ * usePlaytestResponse().
15
+ */
16
+ export function usePlaytestAccessGrant(responseId: number) {
17
+ const loading = ref(false);
18
+ const submitting = ref(false);
19
+ const grant = ref<PlaytestAccessGrant | null>(null);
20
+ const genericError = ref<string | null>(null);
21
+ // True once an OTP has been sent for the current signing attempt — lets
22
+ // the waiver UI switch from "send code" to "enter code" without waiting
23
+ // on a grant re-fetch (the grant itself only changes once verified).
24
+ const waiverInitiated = ref(false);
25
+
26
+ function resetErrors() {
27
+ genericError.value = null;
28
+ }
29
+
30
+ function errorMessage(e: any, fallback: string): string {
31
+ return e?.response?.data?.message ?? e?.message ?? fallback;
32
+ }
33
+
34
+ async function loadAccessGrant() {
35
+ loading.value = true;
36
+ try {
37
+ const res = await fetchAccessGrant(responseId);
38
+ grant.value = res.data.data;
39
+ return grant.value;
40
+ } finally {
41
+ loading.value = false;
42
+ }
43
+ }
44
+
45
+ async function initiateWaiver() {
46
+ resetErrors();
47
+ submitting.value = true;
48
+ try {
49
+ await initiateAccessGrantWaiver(responseId);
50
+ waiverInitiated.value = true;
51
+ return true;
52
+ } catch (e: any) {
53
+ genericError.value = errorMessage(e, "Could not send the verification code. Please try again.");
54
+ return false;
55
+ } finally {
56
+ submitting.value = false;
57
+ }
58
+ }
59
+
60
+ async function verifyWaiver(otpCode: string) {
61
+ resetErrors();
62
+ submitting.value = true;
63
+ try {
64
+ const res = await verifyAccessGrantWaiver(responseId, otpCode);
65
+ grant.value = res.data.data;
66
+ return grant.value;
67
+ } catch (e: any) {
68
+ genericError.value = errorMessage(e, "Invalid or expired code.");
69
+ return null;
70
+ } finally {
71
+ submitting.value = false;
72
+ }
73
+ }
74
+
75
+ return {
76
+ loading,
77
+ submitting,
78
+ grant,
79
+ genericError,
80
+ waiverInitiated,
81
+ loadAccessGrant,
82
+ initiateWaiver,
83
+ verifyWaiver,
84
+ resetErrors,
85
+ };
86
+ }
87
+
88
+ export type PlaytestAccessGrantComposable = ReturnType<typeof usePlaytestAccessGrant>;
@@ -0,0 +1,191 @@
1
+ import { ref } from "vue";
2
+ import {
3
+ requestRecordingUploadUrl,
4
+ fetchRecording,
5
+ storeRecordingMarker,
6
+ submitPlaytestResponse,
7
+ type PlaytestRecording,
8
+ type PlaytestRecordingMarker,
9
+ } from "../services/playtestTesterService";
10
+
11
+ /**
12
+ * Browser-side capture (MediaRecorder over getDisplayMedia — screen + audio)
13
+ * and upload lifecycle for a "Video Playtest" ("minutagem") response. One
14
+ * instance per campaign visit, mirroring usePlaytestResponse().
15
+ *
16
+ * Deliberately minimal per the game-access/minutagem plan's cost controls:
17
+ * no client-side trimming/editing, no separate mic-track mixing (system/tab
18
+ * audio only — a real limitation, not an oversight), duration capped
19
+ * server-side (config playtest.video.max_duration_seconds).
20
+ */
21
+ export function usePlaytestVideoRecording(campaignId: number) {
22
+ const capturing = ref(false);
23
+ const uploading = ref(false);
24
+ const uploadProgress = ref(0);
25
+ const submitting = ref(false);
26
+ const genericError = ref<string | null>(null);
27
+
28
+ const recordingId = ref<number | null>(null);
29
+ const recording = ref<PlaytestRecording | null>(null);
30
+ const recordedBlob = ref<Blob | null>(null);
31
+ const recordedDurationSeconds = ref(0);
32
+ const markers = ref<{ timestamp_seconds: number; comment: string; severity?: string }[]>([]);
33
+
34
+ let mediaRecorder: MediaRecorder | null = null;
35
+ let chunks: BlobPart[] = [];
36
+ let captureStartedAt = 0;
37
+
38
+ function resetError() {
39
+ genericError.value = null;
40
+ }
41
+
42
+ function errorMessage(e: any, fallback: string): string {
43
+ return e?.response?.data?.message ?? e?.message ?? fallback;
44
+ }
45
+
46
+ /** Also used to add a marker mid-recording — timestamp is relative to capture start. */
47
+ function elapsedSeconds(): number {
48
+ return captureStartedAt ? Math.floor((Date.now() - captureStartedAt) / 1000) : 0;
49
+ }
50
+
51
+ async function startCapture() {
52
+ resetError();
53
+ try {
54
+ const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
55
+ chunks = [];
56
+ mediaRecorder = new MediaRecorder(stream, { mimeType: "video/webm" });
57
+ mediaRecorder.ondataavailable = (e) => {
58
+ if (e.data.size > 0) chunks.push(e.data);
59
+ };
60
+ mediaRecorder.onstop = () => {
61
+ recordedBlob.value = new Blob(chunks, { type: "video/webm" });
62
+ recordedDurationSeconds.value = elapsedSeconds();
63
+ stream.getTracks().forEach((t) => t.stop());
64
+ };
65
+ mediaRecorder.start(1000);
66
+ captureStartedAt = Date.now();
67
+ capturing.value = true;
68
+ } catch (e: any) {
69
+ genericError.value = errorMessage(e, "Could not start screen recording. Please allow screen-share permission and try again.");
70
+ }
71
+ }
72
+
73
+ function stopCapture() {
74
+ if (mediaRecorder && mediaRecorder.state !== "inactive") {
75
+ mediaRecorder.stop();
76
+ }
77
+ capturing.value = false;
78
+ }
79
+
80
+ /** Adds a marker anchored to the current elapsed recording time. */
81
+ function markNow(comment: string, severity?: string) {
82
+ markers.value.push({ timestamp_seconds: elapsedSeconds(), comment, severity });
83
+ }
84
+
85
+ /** Requests the upload slot and uploads the recorded blob directly to the video provider. */
86
+ async function uploadRecording(): Promise<boolean> {
87
+ if (!recordedBlob.value) {
88
+ genericError.value = "No recording to upload yet.";
89
+ return false;
90
+ }
91
+ resetError();
92
+ uploading.value = true;
93
+ uploadProgress.value = 0;
94
+ try {
95
+ const { data } = await requestRecordingUploadUrl(campaignId);
96
+ recordingId.value = data.data.recording_id;
97
+
98
+ const form = new FormData();
99
+ form.append("file", recordedBlob.value, "recording.webm");
100
+
101
+ await new Promise<void>((resolve, reject) => {
102
+ const xhr = new XMLHttpRequest();
103
+ xhr.open("POST", data.data.upload_url);
104
+ xhr.upload.onprogress = (e) => {
105
+ if (e.lengthComputable) uploadProgress.value = Math.round((e.loaded / e.total) * 100);
106
+ };
107
+ xhr.onload = () => (xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new Error(`Upload failed (${xhr.status})`)));
108
+ xhr.onerror = () => reject(new Error("Upload failed — network error."));
109
+ xhr.send(form);
110
+ });
111
+
112
+ return true;
113
+ } catch (e: any) {
114
+ genericError.value = errorMessage(e, "Could not upload the recording. Please try again.");
115
+ return false;
116
+ } finally {
117
+ uploading.value = false;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Submits the response once the backend confirms the recording is ready.
123
+ * Processing is async (the provider's webhook updates our DB after
124
+ * upload), so this retries a few times with a short delay instead of
125
+ * failing on the first "not ready yet" — the backend (not this client)
126
+ * is the source of truth for readiness.
127
+ */
128
+ async function submitVideoResponse(
129
+ meta: { time_on_task_seconds?: number; device?: string; os?: string } = {},
130
+ maxAttempts = 5,
131
+ delayMs = 4000,
132
+ ) {
133
+ if (!recordingId.value) {
134
+ genericError.value = "Upload the recording before submitting.";
135
+ return null;
136
+ }
137
+ resetError();
138
+ submitting.value = true;
139
+ try {
140
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
141
+ try {
142
+ const { data } = await submitPlaytestResponse(campaignId, {
143
+ answers: {},
144
+ recording_id: recordingId.value,
145
+ ...meta,
146
+ });
147
+ for (const m of markers.value) {
148
+ await storeRecordingMarker(data.id, m as any);
149
+ }
150
+ return data;
151
+ } catch (e: any) {
152
+ if (attempt === maxAttempts) throw e;
153
+ await new Promise((r) => setTimeout(r, delayMs));
154
+ }
155
+ }
156
+ return null;
157
+ } catch (e: any) {
158
+ genericError.value = errorMessage(e, "The recording isn't ready yet — please wait a moment and try again.");
159
+ return null;
160
+ } finally {
161
+ submitting.value = false;
162
+ }
163
+ }
164
+
165
+ async function loadRecording(responseId: number) {
166
+ const { data } = await fetchRecording(responseId);
167
+ recording.value = data.data;
168
+ return recording.value;
169
+ }
170
+
171
+ return {
172
+ capturing,
173
+ uploading,
174
+ uploadProgress,
175
+ submitting,
176
+ genericError,
177
+ recordingId,
178
+ recording,
179
+ recordedBlob,
180
+ recordedDurationSeconds,
181
+ markers,
182
+ startCapture,
183
+ stopCapture,
184
+ markNow,
185
+ uploadRecording,
186
+ submitVideoResponse,
187
+ loadRecording,
188
+ };
189
+ }
190
+
191
+ export type PlaytestVideoRecordingComposable = ReturnType<typeof usePlaytestVideoRecording>;
package/locales/de.json CHANGED
@@ -204,6 +204,7 @@
204
204
  "required_fields": "Bitte alle Pflichtfelder ausfüllen",
205
205
  "plan": "Deckungspläne",
206
206
  "plan_placeholder": "Beschreibe deinen Berichterstattungsplan: wo du über das Spiel streamen oder posten wirst, dein Publikum und das geplante Veröffentlichungsdatum",
207
+ "notify_opt_in": "Benachrichtige mich über neue Key-Kampagnen",
207
208
  "resources_title": "Ressourcen",
208
209
  "press_kit_link": "Presse-Kit",
209
210
  "review_guideline": "Review-Richtlinie",
package/locales/en.json CHANGED
@@ -204,6 +204,7 @@
204
204
  "required_fields": "Please fill in all required fields",
205
205
  "plan": "Coverage plans",
206
206
  "plan_placeholder": "Describe your coverage plan: where you will stream or post about this game, your audience, and expected publish date",
207
+ "notify_opt_in": "Notify me about new key campaigns",
207
208
  "resources_title": "Resources",
208
209
  "press_kit_link": "Press Kit",
209
210
  "review_guideline": "Review guideline",
@@ -204,6 +204,7 @@
204
204
  "required_fields": "Por favor, preencha todos os campos obrigatórios",
205
205
  "plan": " Planos de cobertura",
206
206
  "plan_placeholder": "Descreva seu plano de cobertura: onde vai fazer stream ou publicar sobre o jogo, seu público e a data prevista de publicação",
207
+ "notify_opt_in": "Quero ser notificado sobre novas campanhas de chaves",
207
208
  "resources_title": "Recursos",
208
209
  "press_kit_link": "Press Kit",
209
210
  "review_guideline": "Diretriz de review",
package/locales/ro.json CHANGED
@@ -204,6 +204,7 @@
204
204
  "required_fields": "Vă rugăm să completați toate câmpurile obligatorii",
205
205
  "plan": "Planuri de acoperire",
206
206
  "plan_placeholder": "Descrie planul tău de acoperire: unde vei face stream sau vei posta despre joc, publicul tău și data estimată de publicare",
207
+ "notify_opt_in": "Anunță-mă despre noile campanii de chei",
207
208
  "resources_title": "Resurse",
208
209
  "press_kit_link": "Press Kit",
209
210
  "review_guideline": "Ghid de recenzie",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mundogamernetwork/shared-ui",
3
- "version": "1.4.2",
3
+ "version": "1.5.0",
4
4
  "description": "Mundo Gamer Network - Shared UI Layer (Nuxt 3)",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",