@mundogamernetwork/shared-ui 1.6.0 → 1.8.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 +4 -3
- package/components/indie-wall/IndieWallLeaderboard.vue +17 -4
- package/components/playtest/video/RecordedSessionPlayer.vue +40 -5
- package/components/playtest/video/VideoPlaytestCapture.vue +38 -2
- package/composables/usePlaytestVideoRecording.ts +106 -3
- package/error.vue +7 -7
- 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/package.json +3 -1
- package/pages/mural/[slug].vue +25 -5
- package/pages/presskit/game/[slug].vue +7 -1
- package/pages/widget/wall/[id].vue +294 -0
- /package/components/errors/{401.vue → Error401.vue} +0 -0
- /package/components/errors/{403.vue → Error403.vue} +0 -0
- /package/components/errors/{404.vue → Error404.vue} +0 -0
- /package/components/errors/{500.vue → Error500.vue} +0 -0
- /package/components/errors/{503.vue → Error503.vue} +0 -0
- /package/components/errors/{504.vue → Error504.vue} +0 -0
|
@@ -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:
|
|
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
|
|
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
|
|
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>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
// Playback + clickable marker timeline for a Video Playtest recording.
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
import { ref } from "vue";
|
|
3
|
+
// Safari plays HLS natively; every other browser needs hls.js (MediaSource
|
|
4
|
+
// Extensions) — falls back to a direct-link note only if neither path works
|
|
5
|
+
// (very old browsers without MSE support).
|
|
6
|
+
import { ref, onMounted, onBeforeUnmount, watch } from "vue";
|
|
7
|
+
import Hls from "hls.js";
|
|
7
8
|
|
|
8
9
|
const props = defineProps<{
|
|
9
10
|
playbackUrl: string | null;
|
|
@@ -14,6 +15,41 @@ const props = defineProps<{
|
|
|
14
15
|
const videoEl = ref<HTMLVideoElement | null>(null);
|
|
15
16
|
const playbackError = ref(false);
|
|
16
17
|
|
|
18
|
+
let hls: Hls | null = null;
|
|
19
|
+
|
|
20
|
+
function destroyPlayer() {
|
|
21
|
+
if (hls) {
|
|
22
|
+
hls.destroy();
|
|
23
|
+
hls = null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function setupPlayback() {
|
|
28
|
+
destroyPlayer();
|
|
29
|
+
playbackError.value = false;
|
|
30
|
+
|
|
31
|
+
const video = videoEl.value;
|
|
32
|
+
if (!video || !props.playbackUrl) return;
|
|
33
|
+
|
|
34
|
+
if (Hls.isSupported()) {
|
|
35
|
+
hls = new Hls();
|
|
36
|
+
hls.on(Hls.Events.ERROR, (_event, data) => {
|
|
37
|
+
if (data.fatal) playbackError.value = true;
|
|
38
|
+
});
|
|
39
|
+
hls.loadSource(props.playbackUrl);
|
|
40
|
+
hls.attachMedia(video);
|
|
41
|
+
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
|
|
42
|
+
// Safari and other browsers with native HLS support.
|
|
43
|
+
video.src = props.playbackUrl;
|
|
44
|
+
} else {
|
|
45
|
+
playbackError.value = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
onMounted(setupPlayback);
|
|
50
|
+
watch(() => props.playbackUrl, setupPlayback);
|
|
51
|
+
onBeforeUnmount(destroyPlayer);
|
|
52
|
+
|
|
17
53
|
function seekTo(seconds: number) {
|
|
18
54
|
if (videoEl.value) {
|
|
19
55
|
videoEl.value.currentTime = seconds;
|
|
@@ -46,7 +82,6 @@ function severityClass(severity?: string | null): string {
|
|
|
46
82
|
<template v-else>
|
|
47
83
|
<video
|
|
48
84
|
ref="videoEl"
|
|
49
|
-
:src="playbackUrl"
|
|
50
85
|
controls
|
|
51
86
|
class="recorded-session-player__video"
|
|
52
87
|
@error="playbackError = true"
|
|
@@ -16,6 +16,8 @@ const emit = defineEmits<{
|
|
|
16
16
|
const rec = usePlaytestVideoRecording(props.campaignId);
|
|
17
17
|
|
|
18
18
|
const markerDraft = ref("");
|
|
19
|
+
const submitted = ref(false);
|
|
20
|
+
const retryingMarkers = ref(false);
|
|
19
21
|
const step = computed<"record" | "review" | "upload" | "done">(() => {
|
|
20
22
|
if (rec.recordedBlob.value && !rec.recordingId.value) return "review";
|
|
21
23
|
if (rec.recordingId.value) return "upload";
|
|
@@ -30,7 +32,23 @@ function addMarker() {
|
|
|
30
32
|
|
|
31
33
|
async function submit() {
|
|
32
34
|
const result = await rec.submitVideoResponse();
|
|
33
|
-
if (result)
|
|
35
|
+
if (!result) return;
|
|
36
|
+
submitted.value = true;
|
|
37
|
+
if (rec.markerSubmitErrors.value === 0) emit("submitted");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function retryMarkers() {
|
|
41
|
+
retryingMarkers.value = true;
|
|
42
|
+
try {
|
|
43
|
+
await rec.retryPendingMarkers();
|
|
44
|
+
if (rec.markerSubmitErrors.value === 0) emit("submitted");
|
|
45
|
+
} finally {
|
|
46
|
+
retryingMarkers.value = false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function continueAfterSubmit() {
|
|
51
|
+
emit("submitted");
|
|
34
52
|
}
|
|
35
53
|
|
|
36
54
|
function formatSeconds(s: number): string {
|
|
@@ -97,7 +115,7 @@ function formatSeconds(s: number): string {
|
|
|
97
115
|
</template>
|
|
98
116
|
|
|
99
117
|
<!-- Upload done, waiting on provider processing — submit -->
|
|
100
|
-
<template v-else-if="step === 'upload'">
|
|
118
|
+
<template v-else-if="step === 'upload' && !submitted">
|
|
101
119
|
<p class="video-capture__hint">
|
|
102
120
|
{{ $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
121
|
</p>
|
|
@@ -107,6 +125,23 @@ function formatSeconds(s: number): string {
|
|
|
107
125
|
: $t("playtest.video_capture.submit", "Submit response") }}
|
|
108
126
|
</button>
|
|
109
127
|
</template>
|
|
128
|
+
|
|
129
|
+
<!-- Response submitted, but one or more markers failed to save — offer a retry instead of silently dropping them -->
|
|
130
|
+
<template v-else-if="submitted && rec.markerSubmitErrors.value > 0">
|
|
131
|
+
<p class="video-capture__hint video-capture__error">
|
|
132
|
+
{{ $t("playtest.video_capture.marker_save_failed", "Response submitted, but {v} note(s) failed to save.", { v: rec.markerSubmitErrors.value }) }}
|
|
133
|
+
</p>
|
|
134
|
+
<div class="video-capture__actions">
|
|
135
|
+
<button type="button" class="btn primary" :disabled="retryingMarkers" @click="retryMarkers">
|
|
136
|
+
{{ retryingMarkers
|
|
137
|
+
? $t("playtest.video_capture.retrying", "Retrying…")
|
|
138
|
+
: $t("playtest.video_capture.retry_markers", "Retry saving notes") }}
|
|
139
|
+
</button>
|
|
140
|
+
<button type="button" class="btn" @click="continueAfterSubmit">
|
|
141
|
+
{{ $t("playtest.video_capture.continue_anyway", "Continue anyway") }}
|
|
142
|
+
</button>
|
|
143
|
+
</div>
|
|
144
|
+
</template>
|
|
110
145
|
</div>
|
|
111
146
|
</template>
|
|
112
147
|
|
|
@@ -130,6 +165,7 @@ function formatSeconds(s: number): string {
|
|
|
130
165
|
|
|
131
166
|
&__actions {
|
|
132
167
|
display: flex;
|
|
168
|
+
gap: 8px;
|
|
133
169
|
}
|
|
134
170
|
|
|
135
171
|
&__recording {
|
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
* audio only — a real limitation, not an oversight), duration capped
|
|
19
19
|
* server-side (config playtest.video.max_duration_seconds).
|
|
20
20
|
*/
|
|
21
|
+
type PendingMarker = { timestamp_seconds: number; comment: string; severity?: string };
|
|
22
|
+
|
|
21
23
|
export function usePlaytestVideoRecording(campaignId: number) {
|
|
22
24
|
const capturing = ref(false);
|
|
23
25
|
const uploading = ref(false);
|
|
@@ -29,12 +31,45 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
29
31
|
const recording = ref<PlaytestRecording | null>(null);
|
|
30
32
|
const recordedBlob = ref<Blob | null>(null);
|
|
31
33
|
const recordedDurationSeconds = ref(0);
|
|
32
|
-
const markers = ref<
|
|
34
|
+
const markers = ref<PendingMarker[]>([]);
|
|
35
|
+
// Markers a submitted response still needs posted — set when the post-submit
|
|
36
|
+
// loop hits an error partway through, so the UI can offer a retry instead of
|
|
37
|
+
// silently dropping the remainder (see storeRecordingMarker loop below).
|
|
38
|
+
const markerSubmitErrors = ref(0);
|
|
33
39
|
|
|
34
40
|
let mediaRecorder: MediaRecorder | null = null;
|
|
35
41
|
let chunks: BlobPart[] = [];
|
|
36
42
|
let captureStartedAt = 0;
|
|
37
43
|
|
|
44
|
+
// Markers only ever live in this Vue ref + localStorage until they're
|
|
45
|
+
// posted — a crash between adding a marker and a successful submit loses
|
|
46
|
+
// nothing beyond that point (the recording Blob itself is still
|
|
47
|
+
// memory-only and can't survive a reload; that's a separate, harder
|
|
48
|
+
// problem this doesn't attempt to solve).
|
|
49
|
+
const storageKey = `mgn_playtest_pending_markers_${campaignId}`;
|
|
50
|
+
|
|
51
|
+
function persistMarkers() {
|
|
52
|
+
try {
|
|
53
|
+
if (markers.value.length) {
|
|
54
|
+
localStorage.setItem(storageKey, JSON.stringify(markers.value));
|
|
55
|
+
} else {
|
|
56
|
+
localStorage.removeItem(storageKey);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// localStorage unavailable (private mode / quota) — best-effort only.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Restores markers left over from a previous capture on this campaign that never made it to a submit. */
|
|
64
|
+
function restorePendingMarkers(): PendingMarker[] {
|
|
65
|
+
try {
|
|
66
|
+
const raw = localStorage.getItem(storageKey);
|
|
67
|
+
return raw ? (JSON.parse(raw) as PendingMarker[]) : [];
|
|
68
|
+
} catch {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
38
73
|
function resetError() {
|
|
39
74
|
genericError.value = null;
|
|
40
75
|
}
|
|
@@ -50,6 +85,12 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
50
85
|
|
|
51
86
|
async function startCapture() {
|
|
52
87
|
resetError();
|
|
88
|
+
// A previous capture on this campaign may have left markers behind
|
|
89
|
+
// (crash/reload before a successful submit) — recover them rather than
|
|
90
|
+
// silently starting from an empty list.
|
|
91
|
+
const leftover = restorePendingMarkers();
|
|
92
|
+
if (leftover.length) markers.value = leftover;
|
|
93
|
+
|
|
53
94
|
try {
|
|
54
95
|
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true, audio: true });
|
|
55
96
|
chunks = [];
|
|
@@ -80,6 +121,7 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
80
121
|
/** Adds a marker anchored to the current elapsed recording time. */
|
|
81
122
|
function markNow(comment: string, severity?: string) {
|
|
82
123
|
markers.value.push({ timestamp_seconds: elapsedSeconds(), comment, severity });
|
|
124
|
+
persistMarkers();
|
|
83
125
|
}
|
|
84
126
|
|
|
85
127
|
/** Requests the upload slot and uploads the recorded blob directly to the video provider. */
|
|
@@ -118,6 +160,60 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
118
160
|
}
|
|
119
161
|
}
|
|
120
162
|
|
|
163
|
+
let lastSubmittedResponseId: number | null = null;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Marker timestamps are wall-clock elapsed time from when capture started,
|
|
167
|
+
* not the real position in the final encoded file — any delay before
|
|
168
|
+
* MediaRecorder actually starts encoding, or a hiccup mid-recording, drifts
|
|
169
|
+
* them apart. Once the provider confirms the real duration (post-submit,
|
|
170
|
+
* since submit only succeeds once the recording is 'ready'), scale every
|
|
171
|
+
* marker proportionally against the wall-clock duration we measured — a
|
|
172
|
+
* practical correction, not frame-perfect, but far closer than raw elapsed
|
|
173
|
+
* seconds once the two durations diverge.
|
|
174
|
+
*/
|
|
175
|
+
function correctForDrift(realDurationSeconds: number | null | undefined) {
|
|
176
|
+
if (!realDurationSeconds || !recordedDurationSeconds.value) return;
|
|
177
|
+
const scale = realDurationSeconds / recordedDurationSeconds.value;
|
|
178
|
+
if (!isFinite(scale) || scale <= 0 || scale === 1) return;
|
|
179
|
+
markers.value = markers.value.map((m) => ({
|
|
180
|
+
...m,
|
|
181
|
+
timestamp_seconds: Math.min(realDurationSeconds, Math.max(0, Math.round(m.timestamp_seconds * scale))),
|
|
182
|
+
}));
|
|
183
|
+
persistMarkers();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Posts whatever's left in markers.value for responseId, one at a time.
|
|
188
|
+
* A failure on one marker does NOT abort the rest — each is independent
|
|
189
|
+
* feedback, so losing one shouldn't cost the others. Successfully-posted
|
|
190
|
+
* markers are removed from the pending list (and localStorage) as they
|
|
191
|
+
* succeed; anything left after this call can be retried via
|
|
192
|
+
* retryPendingMarkers() instead of being silently dropped.
|
|
193
|
+
*/
|
|
194
|
+
async function postPendingMarkers(responseId: number) {
|
|
195
|
+
lastSubmittedResponseId = responseId;
|
|
196
|
+
const remaining: PendingMarker[] = [];
|
|
197
|
+
let failures = 0;
|
|
198
|
+
for (const m of markers.value) {
|
|
199
|
+
try {
|
|
200
|
+
await storeRecordingMarker(responseId, m as any);
|
|
201
|
+
} catch {
|
|
202
|
+
remaining.push(m);
|
|
203
|
+
failures++;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
markers.value = remaining;
|
|
207
|
+
markerSubmitErrors.value = failures;
|
|
208
|
+
persistMarkers();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Retries any markers that failed to post during submitVideoResponse(). */
|
|
212
|
+
async function retryPendingMarkers() {
|
|
213
|
+
if (!lastSubmittedResponseId || !markers.value.length) return;
|
|
214
|
+
await postPendingMarkers(lastSubmittedResponseId);
|
|
215
|
+
}
|
|
216
|
+
|
|
121
217
|
/**
|
|
122
218
|
* Submits the response once the backend confirms the recording is ready.
|
|
123
219
|
* Processing is async (the provider's webhook updates our DB after
|
|
@@ -135,6 +231,7 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
135
231
|
return null;
|
|
136
232
|
}
|
|
137
233
|
resetError();
|
|
234
|
+
markerSubmitErrors.value = 0;
|
|
138
235
|
submitting.value = true;
|
|
139
236
|
try {
|
|
140
237
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
@@ -144,9 +241,13 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
144
241
|
recording_id: recordingId.value,
|
|
145
242
|
...meta,
|
|
146
243
|
});
|
|
147
|
-
|
|
148
|
-
await
|
|
244
|
+
try {
|
|
245
|
+
const fetched = await fetchRecording(data.id);
|
|
246
|
+
correctForDrift(fetched.data.data?.duration_seconds ?? null);
|
|
247
|
+
} catch {
|
|
248
|
+
// Non-fatal — fall back to uncorrected (wall-clock) timestamps.
|
|
149
249
|
}
|
|
250
|
+
await postPendingMarkers(data.id);
|
|
150
251
|
return data;
|
|
151
252
|
} catch (e: any) {
|
|
152
253
|
if (attempt === maxAttempts) throw e;
|
|
@@ -179,11 +280,13 @@ export function usePlaytestVideoRecording(campaignId: number) {
|
|
|
179
280
|
recordedBlob,
|
|
180
281
|
recordedDurationSeconds,
|
|
181
282
|
markers,
|
|
283
|
+
markerSubmitErrors,
|
|
182
284
|
startCapture,
|
|
183
285
|
stopCapture,
|
|
184
286
|
markNow,
|
|
185
287
|
uploadRecording,
|
|
186
288
|
submitVideoResponse,
|
|
289
|
+
retryPendingMarkers,
|
|
187
290
|
loadRecording,
|
|
188
291
|
};
|
|
189
292
|
}
|
package/error.vue
CHANGED
|
@@ -51,13 +51,13 @@ onUnmounted(() => {
|
|
|
51
51
|
|
|
52
52
|
<template>
|
|
53
53
|
<NuxtLayout>
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
<
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
<
|
|
60
|
-
<
|
|
54
|
+
<Error401 v-if="statusCodeError === HttpStatusCode.Unauthorized" />
|
|
55
|
+
<Error403 v-else-if="statusCodeError === HttpStatusCode.Forbidden" />
|
|
56
|
+
<Error404 v-else-if="statusCodeError === HttpStatusCode.NotFound" />
|
|
57
|
+
<Error500 v-else-if="statusCodeError === HttpStatusCode.InternalServerError" />
|
|
58
|
+
<Error503 v-else-if="statusCodeError === HttpStatusCode.ServiceUnavailable" />
|
|
59
|
+
<Error504 v-else-if="statusCodeError === HttpStatusCode.GatewayTimeout" />
|
|
60
|
+
<Error500 v-else />
|
|
61
61
|
|
|
62
62
|
<p v-if="isRecoverableError" class="recovery-line">
|
|
63
63
|
{{ $t("errors.recovery_hint") }}
|
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
|
+
}
|