mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-05 00:46:00 +08:00
Translate auth flows and PM UI to English
This commit is contained in:
@@ -10,55 +10,80 @@
|
||||
>
|
||||
<section class="cookie-consent-panel">
|
||||
<header class="cookie-consent-header">
|
||||
<h2 id="cookie-consent-title">Cookies & Analysen</h2>
|
||||
<h2 id="cookie-consent-title">{{ isGerman ? 'Cookies & Analysen' : 'Cookies & analytics' }}</h2>
|
||||
<p id="cookie-consent-description">
|
||||
Wir verwenden notwendige Cookies, damit die Website zuverlässig funktioniert. Darüber hinaus würden wir gern optionale
|
||||
Analyse-Cookies von Hotjar einsetzen, um das Nutzungserlebnis zu verbessern. Sie entscheiden, ob Sie diese zulassen –
|
||||
alle Details finden Sie in unserer
|
||||
<a href="/datenschutz" target="_blank" rel="noopener">Datenschutzerklärung</a>.
|
||||
<template v-if="isGerman">
|
||||
Wir verwenden notwendige Cookies, damit die Website zuverlässig funktioniert. Darüber hinaus würden wir gern optionale
|
||||
Analyse-Cookies von Hotjar einsetzen, um das Nutzungserlebnis zu verbessern. Sie entscheiden, ob Sie diese zulassen – alle
|
||||
Details finden Sie in unserer
|
||||
</template>
|
||||
<template v-else>
|
||||
We use essential cookies to keep the site reliable. We would also like to use optional Hotjar analytics cookies to improve your
|
||||
experience. You decide whether to allow them—see our
|
||||
</template>
|
||||
<a href="/datenschutz" target="_blank" rel="noopener">{{ isGerman ? 'Datenschutzerklärung' : 'privacy policy' }}</a>.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="cookie-consent-options" role="list">
|
||||
<article class="cookie-option is-required" role="listitem">
|
||||
<div class="cookie-option-text">
|
||||
<h3>Notwendige Cookies</h3>
|
||||
<p>Speichern Ihre Auswahl und sorgen für sichere Anmeldung sowie eine stabile Grundfunktionalität.</p>
|
||||
<h3>{{ isGerman ? 'Notwendige Cookies' : 'Essential cookies' }}</h3>
|
||||
<p>
|
||||
{{
|
||||
isGerman
|
||||
? 'Speichern Ihre Auswahl und sorgen für sichere Anmeldung sowie eine stabile Grundfunktionalität.'
|
||||
: 'Store your selection and enable secure sign-in plus a stable core experience.'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<span class="cookie-option-badge" aria-hidden="true">Immer aktiv</span>
|
||||
<span class="cookie-option-badge" aria-hidden="true">{{ isGerman ? 'Immer aktiv' : 'Always on' }}</span>
|
||||
</article>
|
||||
|
||||
<article class="cookie-option" role="listitem">
|
||||
<div class="cookie-option-text">
|
||||
<h3>Analyse (Hotjar)</h3>
|
||||
<p>Hilft uns, Nutzungsverhalten anonym auszuwerten und unsere Inhalte gezielt zu verbessern.</p>
|
||||
<h3>{{ isGerman ? 'Analyse (Hotjar)' : 'Analytics (Hotjar)' }}</h3>
|
||||
<p>
|
||||
{{
|
||||
isGerman
|
||||
? 'Hilft uns, Nutzungsverhalten anonym auszuwerten und unsere Inhalte gezielt zu verbessern.'
|
||||
: 'Helps us analyse anonymised usage to improve content and product decisions.'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="cookie-toggle"
|
||||
role="switch"
|
||||
:aria-checked="analyticsSelection"
|
||||
@click="toggleAnalytics"
|
||||
type="button"
|
||||
class="cookie-toggle"
|
||||
role="switch"
|
||||
:aria-checked="analyticsSelection"
|
||||
@click="toggleAnalytics"
|
||||
>
|
||||
<span class="cookie-toggle-track" :class="{ 'is-active': analyticsSelection }">
|
||||
<span class="cookie-toggle-thumb" />
|
||||
</span>
|
||||
<span class="sr-only">
|
||||
Analyse-Cookies {{ analyticsSelection ? 'deaktivieren' : 'aktivieren' }}
|
||||
{{
|
||||
isGerman
|
||||
? `Analyse-Cookies ${analyticsSelection ? 'deaktivieren' : 'aktivieren'}`
|
||||
: `${analyticsSelection ? 'Disable' : 'Enable'} analytics cookies`
|
||||
}}
|
||||
</span>
|
||||
</button>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<p class="cookie-consent-note">
|
||||
Optionales Tracking wird erst nach Ihrer Zustimmung geladen. Sie können Ihre Entscheidung jederzeit über „Cookie-Einstellungen“
|
||||
unten links widerrufen.
|
||||
{{
|
||||
isGerman
|
||||
? 'Optionales Tracking wird erst nach Ihrer Zustimmung geladen. Sie können Ihre Entscheidung jederzeit über „Cookie-Einstellungen“ unten links widerrufen.'
|
||||
: 'Optional tracking only loads after you consent. You can revisit your choice at any time via “Cookie settings” in the lower left.'
|
||||
}}
|
||||
</p>
|
||||
|
||||
<div class="cookie-consent-actions">
|
||||
<button type="button" class="cookie-button ghost" @click="handleRejectAll">Nur notwendige</button>
|
||||
<button type="button" class="cookie-button secondary" @click="handleSave">Auswahl speichern</button>
|
||||
<button type="button" class="cookie-button primary" @click="handleAcceptAll">Alle akzeptieren</button>
|
||||
<button type="button" class="cookie-button ghost" @click="handleRejectAll">{{ isGerman ? 'Nur notwendige' : 'Only necessary' }}</button>
|
||||
<button type="button" class="cookie-button secondary" @click="handleSave">{{ isGerman ? 'Auswahl speichern' : 'Save selection' }}</button>
|
||||
<button type="button" class="cookie-button primary" @click="handleAcceptAll">{{ isGerman ? 'Alle akzeptieren' : 'Accept all' }}</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -66,19 +91,22 @@
|
||||
|
||||
<transition name="cookie-manage">
|
||||
<button v-if="showManageButton" type="button" class="cookie-manage-button" @click="openManager">
|
||||
Cookie-Einstellungen
|
||||
{{ isGerman ? 'Cookie-Einstellungen' : 'Cookie settings' }}
|
||||
</button>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useState } from '#imports';
|
||||
|
||||
const { hasConsent, analyticsEnabled, acceptAll, rejectAll, savePreferences } = useCookieConsent();
|
||||
|
||||
const isDialogVisible = ref(!hasConsent.value);
|
||||
const isManuallyOpened = ref(false);
|
||||
const analyticsSelection = ref(analyticsEnabled.value);
|
||||
const locale = useState<'de' | 'en'>('landing-locale', () => 'de');
|
||||
const isGerman = computed(() => locale.value === 'de');
|
||||
|
||||
watch(
|
||||
() => hasConsent.value,
|
||||
|
||||
@@ -2,36 +2,36 @@
|
||||
<div class="min-h-screen bg-[#0b1020] text-white">
|
||||
<div class="mx-auto max-w-5xl px-6 py-12 lg:py-16 space-y-8">
|
||||
<NuxtLink to="/login" class="inline-flex items-center gap-2 text-sm text-white/70 hover:text-cyan-300">
|
||||
<v-icon icon="mdi-arrow-left" size="18" /> Zurück zum Login
|
||||
<v-icon icon="mdi-arrow-left" size="18" /> Back to login
|
||||
</NuxtLink>
|
||||
|
||||
<div class="grid gap-10 lg:grid-cols-[1.1fr_1fr] items-start">
|
||||
<div class="space-y-6">
|
||||
<div class="space-y-4">
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-cyan-300/80">Passworthilfe</p>
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-cyan-300/80">Password support</p>
|
||||
<h1 class="text-3xl md:text-4xl font-semibold">
|
||||
{{ isResetMode ? 'Neues Passwort festlegen' : 'Passwort zurücksetzen' }}
|
||||
{{ isResetMode ? 'Set a new password' : 'Reset your password' }}
|
||||
</h1>
|
||||
<p class="text-white/70">
|
||||
{{
|
||||
isResetMode
|
||||
? 'Lege jetzt ein neues Passwort für deinen Account fest.'
|
||||
: 'Trage deine registrierte E-Mail-Adresse ein und wir senden dir einen Link zum Zurücksetzen.'
|
||||
? 'Set a new password for your account now.'
|
||||
: 'Enter the email you registered with and we will send you a reset link.'
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-white/10 bg-white/5 p-5 space-y-4">
|
||||
<h2 class="text-lg font-semibold">So funktioniert es</h2>
|
||||
<h2 class="text-lg font-semibold">How it works</h2>
|
||||
<ol v-if="!isResetMode" class="space-y-3 text-white/70 text-sm">
|
||||
<li class="flex gap-3"><span class="chip">1</span><span>E-Mail-Adresse eingeben und Link anfordern.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">2</span><span>E-Mail öffnen und auf den Link klicken.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">3</span><span>Neues Passwort vergeben und einloggen.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">1</span><span>Enter your email address and request the link.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">2</span><span>Open the email and click the link.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">3</span><span>Choose a new password and sign in.</span></li>
|
||||
</ol>
|
||||
<ol v-else class="space-y-3 text-white/70 text-sm">
|
||||
<li class="flex gap-3"><span class="chip">1</span><span>Neues, sicheres Passwort auswählen.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">2</span><span>Zur Sicherheit erneut bestätigen.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">3</span><span>Speichern und mit dem neuen Passwort anmelden.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">1</span><span>Choose a new secure password.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">2</span><span>Confirm it for safety.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">3</span><span>Save it and sign in with the new password.</span></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,7 +39,7 @@
|
||||
<div class="rounded-2xl border border-white/10 bg-white/5 p-6 space-y-6">
|
||||
<form v-if="!isResetMode" class="space-y-4" @submit.prevent="submitRequest">
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">E-Mail</label>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Email</label>
|
||||
<input
|
||||
v-model.trim="requestForm.email"
|
||||
type="email"
|
||||
@@ -52,9 +52,9 @@
|
||||
<button type="submit" class="btn btn-primary w-full" :disabled="requestLoading">
|
||||
<span v-if="requestLoading" class="flex items-center justify-center gap-2">
|
||||
<v-progress-circular indeterminate size="16" width="2" color="white" />
|
||||
Link wird gesendet…
|
||||
Sending link…
|
||||
</span>
|
||||
<span v-else>Link anfordern</span>
|
||||
<span v-else>Request link</span>
|
||||
</button>
|
||||
|
||||
<p v-if="requestError" class="text-sm text-red-300">{{ requestError }}</p>
|
||||
@@ -62,22 +62,22 @@
|
||||
v-else-if="requestSuccess"
|
||||
class="rounded-xl border border-emerald-400/30 bg-emerald-400/10 p-4 text-sm text-emerald-100"
|
||||
>
|
||||
Wenn ein Konto zu dieser Adresse existiert, findest du gleich eine E-Mail mit weiteren Schritten in deinem Postfach.
|
||||
If an account exists for this address, you'll soon find an email with the next steps in your inbox.
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div v-if="resetCompleted" class="space-y-4">
|
||||
<div class="rounded-xl border border-emerald-400/30 bg-emerald-400/10 p-4 text-sm text-emerald-100">
|
||||
Dein Passwort wurde erfolgreich geändert. Du kannst dich jetzt mit dem neuen Passwort anmelden.
|
||||
Your password was changed successfully. You can now sign in with the new password.
|
||||
</div>
|
||||
<NuxtLink to="/login" class="btn btn-primary w-full">Zum Login</NuxtLink>
|
||||
<NuxtLink to="/login" class="btn btn-primary w-full">Back to login</NuxtLink>
|
||||
</div>
|
||||
|
||||
<form v-else class="space-y-4" @submit.prevent="submitReset">
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Neues Passwort</label>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">New password</label>
|
||||
<input
|
||||
v-model="resetForm.password"
|
||||
type="password"
|
||||
@@ -86,10 +86,10 @@
|
||||
autocomplete="new-password"
|
||||
class="mt-1 w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 outline-none focus:border-cyan-400"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-white/50">Mindestens 8 Zeichen, gerne inkl. Zahl & Sonderzeichen.</p>
|
||||
<p class="mt-1 text-xs text-white/50">At least 8 characters, ideally include a number and special character.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Passwort bestätigen</label>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Confirm password</label>
|
||||
<input
|
||||
v-model="resetForm.confirm"
|
||||
type="password"
|
||||
@@ -100,20 +100,20 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="passwordMismatch" class="text-xs text-red-300">Die Passwörter stimmen nicht überein.</p>
|
||||
<p v-if="passwordMismatch" class="text-xs text-red-300">The passwords do not match.</p>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-full" :disabled="resetLoading || !canSubmitReset">
|
||||
<span v-if="resetLoading" class="flex items-center justify-center gap-2">
|
||||
<v-progress-circular indeterminate size="16" width="2" color="white" />
|
||||
Speichere neues Passwort…
|
||||
Saving new password…
|
||||
</span>
|
||||
<span v-else>Passwort speichern</span>
|
||||
<span v-else>Save password</span>
|
||||
</button>
|
||||
|
||||
<p v-if="resetError" class="text-sm text-red-300">{{ resetError }}</p>
|
||||
<p class="text-xs text-white/60">
|
||||
Link funktioniert nicht?
|
||||
<NuxtLink to="/forgot-password" class="text-cyan-300 underline">Neuen Link anfordern</NuxtLink>
|
||||
Link not working?
|
||||
<NuxtLink to="/forgot-password" class="text-cyan-300 underline">Request a new link</NuxtLink>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
@@ -196,7 +196,7 @@ async function submitRequest() {
|
||||
requestSuccess.value = true
|
||||
requestForm.email = ''
|
||||
} catch (err: any) {
|
||||
const message = err?.data?.statusMessage || err?.message || 'Senden fehlgeschlagen'
|
||||
const message = err?.data?.statusMessage || err?.message || 'Sending failed'
|
||||
requestError.value = message
|
||||
} finally {
|
||||
requestLoading.value = false
|
||||
@@ -218,7 +218,7 @@ async function submitReset() {
|
||||
resetForm.password = ''
|
||||
resetForm.confirm = ''
|
||||
} catch (err: any) {
|
||||
const message = err?.data?.statusMessage || err?.message || 'Zurücksetzen fehlgeschlagen'
|
||||
const message = err?.data?.statusMessage || err?.message || 'Reset failed'
|
||||
resetError.value = message
|
||||
} finally {
|
||||
resetLoading.value = false
|
||||
@@ -226,7 +226,7 @@ async function submitReset() {
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: 'OpenSquawk – Passwort zurücksetzen',
|
||||
title: 'OpenSquawk – Reset password',
|
||||
meta: [
|
||||
{ name: 'robots', content: 'noindex,nofollow' },
|
||||
],
|
||||
|
||||
@@ -4,26 +4,26 @@
|
||||
<div class="grid gap-10 lg:grid-cols-[1.1fr_1fr] items-start">
|
||||
<div class="space-y-6">
|
||||
<NuxtLink to="/" class="inline-flex items-center gap-2 text-sm text-white/70 hover:text-cyan-300">
|
||||
<v-icon icon="mdi-arrow-left" size="18" /> Zurück zur Landingpage
|
||||
<v-icon icon="mdi-arrow-left" size="18" /> Back to landing page
|
||||
</NuxtLink>
|
||||
<div class="space-y-4">
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-cyan-300/80">OpenSquawk Alpha Access</p>
|
||||
<h1 class="text-3xl md:text-4xl font-semibold">Melde dich an oder sichere dir deinen Invite</h1>
|
||||
<h1 class="text-3xl md:text-4xl font-semibold">Sign in or claim your invite</h1>
|
||||
<p class="text-white/70">
|
||||
Registrierungen sind nur mit Einladungscode möglich. Codes werden nach Eintrag auf die Warteliste vergeben oder von bestehenden Mitgliedern nach zwei Wochen Nutzungsdauer ausgestellt.
|
||||
Registration requires an invitation code. Codes are issued after joining the waitlist or shared by active members after two weeks of use.
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-white/10 bg-white/5 p-5 space-y-4">
|
||||
<h2 class="text-lg font-semibold">So bekommst du Zugang</h2>
|
||||
<h2 class="text-lg font-semibold">How to get access</h2>
|
||||
<ol class="space-y-3 text-white/70 text-sm">
|
||||
<li class="flex gap-3"><span class="chip">1</span><span>Auf der <NuxtLink to="/#cta" class="text-cyan-300 underline">Warteliste</NuxtLink> eintragen.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">2</span><span>Invite per E-Mail erhalten oder von einem aktiven Nutzer einen Code bekommen.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">3</span><span>Hier registrieren, AGB & Datenschutz bestätigen und direkt loslegen.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">1</span><span>Join the <NuxtLink to="/#cta" class="text-cyan-300 underline">waitlist</NuxtLink>.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">2</span><span>Receive your invite by email or get a code from an active member.</span></li>
|
||||
<li class="flex gap-3"><span class="chip">3</span><span>Register here, accept the terms & privacy policy and start flying.</span></li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-cyan-400/20 bg-cyan-400/10 p-5">
|
||||
<p class="text-sm text-cyan-100">
|
||||
Tipp: Nach 14 Tagen aktiver Nutzung kannst du selbst bis zu zwei Einladungscodes generieren und mit Freunden teilen.
|
||||
Tip: After 14 days of active use you can generate up to two invitation codes yourself and share them with friends.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,14 +42,14 @@
|
||||
:class="mode === 'register' ? 'border-cyan-400 bg-cyan-400/20 text-white' : 'border-white/10 text-white/70 hover:text-white'"
|
||||
@click="mode = 'register'"
|
||||
>
|
||||
Registrieren
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form v-if="mode === 'login'" class="space-y-4" @submit.prevent="submitLogin">
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">E-Mail</label>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Email</label>
|
||||
<input
|
||||
v-model.trim="loginForm.email"
|
||||
type="email"
|
||||
@@ -59,7 +59,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Passwort</label>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Password</label>
|
||||
<input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
@@ -70,7 +70,7 @@
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<NuxtLink to="/forgot-password" class="text-xs text-cyan-300 underline hover:text-cyan-200">
|
||||
Passwort vergessen?
|
||||
Forgot your password?
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
@@ -78,7 +78,7 @@
|
||||
<button type="submit" class="btn btn-primary w-full" :disabled="loginLoading">
|
||||
<span v-if="loginLoading" class="flex items-center justify-center gap-2">
|
||||
<v-progress-circular indeterminate size="16" width="2" color="white" />
|
||||
Anmeldung…
|
||||
Signing you in…
|
||||
</span>
|
||||
<span v-else>Login</span>
|
||||
</button>
|
||||
@@ -94,12 +94,12 @@
|
||||
v-model.trim="registerForm.name"
|
||||
type="text"
|
||||
autocomplete="name"
|
||||
placeholder="Vorname Nachname"
|
||||
placeholder="First Last"
|
||||
class="mt-1 w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 outline-none focus:border-cyan-400"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">E-Mail</label>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Email</label>
|
||||
<input
|
||||
v-model.trim="registerForm.email"
|
||||
type="email"
|
||||
@@ -109,7 +109,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Passwort</label>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Password</label>
|
||||
<input
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
@@ -118,13 +118,13 @@
|
||||
autocomplete="new-password"
|
||||
class="mt-1 w-full rounded-xl border border-white/10 bg-white/5 px-4 py-3 outline-none focus:border-cyan-400"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-white/50">Mindestens 8 Zeichen, gerne inkl. Zahl & Sonderzeichen.</p>
|
||||
<p class="mt-1 text-xs text-white/50">At least 8 characters, ideally include a number and special character.</p>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Einladungscode</label>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40">Invitation code</label>
|
||||
<button type="button" class="text-[11px] text-cyan-300 underline" @click="checkInvitationCode" :disabled="!registerForm.invitationCode">
|
||||
Code prüfen
|
||||
Check code
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
@@ -132,31 +132,31 @@
|
||||
type="text"
|
||||
required
|
||||
class="mt-1 w-full uppercase tracking-widest rounded-xl border border-white/10 bg-white/5 px-4 py-3 outline-none focus:border-cyan-400"
|
||||
placeholder="z.B. ABCD1234"
|
||||
placeholder="e.g. ABCD1234"
|
||||
/>
|
||||
<p v-if="invitationStatus === 'valid'" class="mt-1 text-xs text-green-300">Code ist gültig.</p>
|
||||
<p v-else-if="invitationStatus === 'invalid'" class="mt-1 text-xs text-red-300">Dieser Einladungscode ist ungültig oder bereits verwendet.</p>
|
||||
<p v-else-if="invitationStatus === 'checking'" class="mt-1 text-xs text-white/60">Prüfe Einladungscode…</p>
|
||||
<p v-if="invitationStatus === 'valid'" class="mt-1 text-xs text-green-300">Code is valid.</p>
|
||||
<p v-else-if="invitationStatus === 'invalid'" class="mt-1 text-xs text-red-300">This invitation code is invalid or already used.</p>
|
||||
<p v-else-if="invitationStatus === 'checking'" class="mt-1 text-xs text-white/60">Checking invitation code…</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 text-xs text-white/60">
|
||||
<label class="flex items-start gap-3">
|
||||
<input type="checkbox" v-model="registerForm.acceptTerms" class="mt-1" required />
|
||||
<span>Ich akzeptiere die <NuxtLink to="/agb" class="text-cyan-300 underline">AGB</NuxtLink>.</span>
|
||||
<span>I accept the <NuxtLink to="/agb" class="text-cyan-300 underline">Terms of Service</NuxtLink>.</span>
|
||||
</label>
|
||||
<label class="flex items-start gap-3">
|
||||
<input type="checkbox" v-model="registerForm.acceptPrivacy" class="mt-1" required />
|
||||
<span>Ich habe die <NuxtLink to="/datenschutz" class="text-cyan-300 underline">Datenschutzerklärung</NuxtLink> gelesen und stimme der Datenverarbeitung zu.</span>
|
||||
<span>I have read the <NuxtLink to="/datenschutz" class="text-cyan-300 underline">privacy policy</NuxtLink> and consent to data processing.</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary w-full" :disabled="registerLoading || !canRegister">
|
||||
<span v-if="registerLoading" class="flex items-center justify-center gap-2">
|
||||
<v-progress-circular indeterminate size="16" width="2" color="white" />
|
||||
Registriere…
|
||||
Registering…
|
||||
</span>
|
||||
<span v-else>Account erstellen</span>
|
||||
<span v-else>Create account</span>
|
||||
</button>
|
||||
|
||||
<p v-if="registerError" class="text-sm text-red-300">{{ registerError }}</p>
|
||||
@@ -221,7 +221,7 @@ async function submitLogin() {
|
||||
await auth.fetchUser()
|
||||
router.push('/pm')
|
||||
} catch (err: any) {
|
||||
const message = err?.data?.statusMessage || err?.message || 'Anmeldung fehlgeschlagen'
|
||||
const message = err?.data?.statusMessage || err?.message || 'Login failed'
|
||||
loginError.value = message
|
||||
} finally {
|
||||
loginLoading.value = false
|
||||
@@ -271,7 +271,7 @@ async function submitRegister() {
|
||||
await auth.fetchUser()
|
||||
router.push('/pm')
|
||||
} catch (err: any) {
|
||||
const message = err?.data?.statusMessage || err?.message || 'Registrierung fehlgeschlagen'
|
||||
const message = err?.data?.statusMessage || err?.message || 'Registration failed'
|
||||
registerError.value = message
|
||||
} finally {
|
||||
registerLoading.value = false
|
||||
@@ -279,7 +279,7 @@ async function submitRegister() {
|
||||
}
|
||||
|
||||
useHead({
|
||||
title: 'OpenSquawk – Login & Einladung',
|
||||
title: 'OpenSquawk – Login & invite',
|
||||
meta: [
|
||||
{ name: 'robots', content: 'noindex,nofollow' },
|
||||
],
|
||||
|
||||
108
app/pages/pm.vue
108
app/pages/pm.vue
@@ -21,8 +21,8 @@
|
||||
<v-card class="bg-white/5 backdrop-blur border border-white/10">
|
||||
<v-card-text class="space-y-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">VATSIM Integration</h2>
|
||||
<p class="text-sm text-white/70">Gib deine CID ein um deine gefilterten Flugpläne zu laden</p>
|
||||
<h2 class="text-lg font-semibold">VATSIM integration</h2>
|
||||
<p class="text-sm text-white/70">Enter your CID to load your filtered flight plans.</p>
|
||||
</div>
|
||||
<v-text-field
|
||||
v-model="vatsimId"
|
||||
@@ -41,7 +41,7 @@
|
||||
:loading="loading"
|
||||
@click="loadFlightPlans"
|
||||
>
|
||||
Flugpläne abrufen
|
||||
Load flight plans
|
||||
</v-btn>
|
||||
<v-alert
|
||||
v-if="error"
|
||||
@@ -59,8 +59,8 @@
|
||||
<v-card class="bg-white/5 backdrop-blur border border-white/10">
|
||||
<v-card-text class="space-y-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold">Demo Mode</h2>
|
||||
<p class="text-sm text-white/70">Starte mit einem Beispiel-Flugplan zum Testen</p>
|
||||
<h2 class="text-lg font-semibold">Demo mode</h2>
|
||||
<p class="text-sm text-white/70">Start with a sample flight plan to test the workflow.</p>
|
||||
</div>
|
||||
<v-btn
|
||||
block
|
||||
@@ -68,7 +68,7 @@
|
||||
variant="outlined"
|
||||
@click="startDemoFlight"
|
||||
>
|
||||
Demo starten (DLH39A EDDF→EDDM)
|
||||
Start demo (DLH39A EDDF→EDDM)
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -80,20 +80,20 @@
|
||||
<v-btn icon @click="currentScreen = 'login'" class="text-cyan-300">
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<h2 class="text-lg font-semibold">Verfügbare Flugpläne</h2>
|
||||
<h2 class="text-lg font-semibold">Available flight plans</h2>
|
||||
<v-chip color="cyan" variant="outlined" size="small">{{ vatsimId }}</v-chip>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="text-center py-8">
|
||||
<v-progress-circular indeterminate color="cyan" class="mb-4" />
|
||||
<p class="text-sm text-white/70">Lade Flugpläne von VATSIM...</p>
|
||||
<p class="text-sm text-white/70">Loading flight plans from VATSIM…</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="flightPlans.length === 0" class="text-center py-8">
|
||||
<v-icon size="48" class="text-white/30 mb-4">mdi-airplane-off</v-icon>
|
||||
<p class="text-white/70">Keine Flugpläne gefunden</p>
|
||||
<p class="text-white/70">No flight plans found</p>
|
||||
<v-btn color="cyan" variant="outlined" class="mt-4" @click="currentScreen = 'login'">
|
||||
Zurück
|
||||
Back
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
@@ -135,7 +135,7 @@
|
||||
<v-card-text class="space-y-4">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-white/50">Aktiver Flug</p>
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-white/50">Active flight</p>
|
||||
<h2 class="text-2xl font-semibold">{{ flightContext.callsign || 'N/A' }}</h2>
|
||||
<p class="text-sm text-white/70">{{ flightContext.dep }} → {{ flightContext.dest }}</p>
|
||||
</div>
|
||||
@@ -203,7 +203,7 @@
|
||||
@click="backToSetup"
|
||||
prepend-icon="mdi-airplane-off"
|
||||
>
|
||||
Neuen Flug wählen
|
||||
Choose a different flight
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -247,7 +247,7 @@
|
||||
@click="swapFrequencies"
|
||||
:class="{ 'swap-animation': swapAnimation }"
|
||||
>
|
||||
Frequenzen tauschen
|
||||
Swap frequencies
|
||||
</v-btn>
|
||||
|
||||
<!-- Signal Quality -->
|
||||
@@ -270,7 +270,7 @@
|
||||
<v-card v-if="currentStep" class="bg-white/5 border border-white/10">
|
||||
<v-card-text class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Erwartete Kommunikation</p>
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Expected communication</p>
|
||||
<v-chip color="orange" variant="tonal" size="small">{{ currentStep.frequencyName }}</v-chip>
|
||||
</div>
|
||||
|
||||
@@ -285,7 +285,7 @@
|
||||
<div v-if="currentStep.pilot" class="space-y-2 rounded-2xl bg-blue-500/10 border border-blue-500/20 p-3 text-sm">
|
||||
<div class="flex items-center gap-2 text-blue-300">
|
||||
<v-icon size="16">mdi-account-pilot</v-icon>
|
||||
<span class="text-xs uppercase font-semibold">Pilot (Du)</span>
|
||||
<span class="text-xs uppercase font-semibold">Pilot (you)</span>
|
||||
</div>
|
||||
<p class="font-mono text-white">{{ normalizeExpectedText(currentStep.pilot) }}</p>
|
||||
</div>
|
||||
@@ -314,9 +314,9 @@
|
||||
class="bg-cyan-500/10 text-cyan-100 mb-4"
|
||||
density="compact"
|
||||
>
|
||||
Mikrofonberechtigung erforderlich für Push-To-Talk.
|
||||
Microphone permission required for push-to-talk.
|
||||
<template #append>
|
||||
<v-btn color="cyan" size="small" variant="flat" @click="requestMicAccess">Erlauben</v-btn>
|
||||
<v-btn color="cyan" size="small" variant="flat" @click="requestMicAccess">Allow</v-btn>
|
||||
</template>
|
||||
</v-alert>
|
||||
|
||||
@@ -344,7 +344,7 @@
|
||||
{{ radioMode === 'atc' ? 'ATC' : 'INTERCOM' }}
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-white/60">
|
||||
{{ radioMode === 'atc' ? `Sendet auf ${frequencies.active}` : 'Crew Intercom • Checklist' }}
|
||||
{{ radioMode === 'atc' ? `Transmitting on ${frequencies.active}` : 'Crew Intercom • Checklist' }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -376,7 +376,7 @@
|
||||
prepend-icon="mdi-information-variant"
|
||||
density="comfortable"
|
||||
>
|
||||
ATIS abhören
|
||||
Listen to ATIS
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="cyan"
|
||||
@@ -386,7 +386,7 @@
|
||||
prepend-icon="mdi-progress-check"
|
||||
density="comfortable"
|
||||
>
|
||||
Voller Flug (Simulation)
|
||||
Full flight (simulation)
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card-text>
|
||||
@@ -396,7 +396,7 @@
|
||||
<v-card class="bg-white/5 border border-white/10">
|
||||
<v-card-text class="space-y-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<h3 class="text-lg font-semibold">Frequenzübersicht</h3>
|
||||
<h3 class="text-lg font-semibold">Frequency directory</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<v-progress-circular
|
||||
v-if="airportFrequencyLoading"
|
||||
@@ -421,7 +421,7 @@
|
||||
<v-expansion-panels variant="accordion" class="bg-transparent">
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title class="text-sm text-white/70">
|
||||
Frequenzen für {{ flightContext.dep || 'Abflug' }}
|
||||
Frequencies for {{ flightContext.dep || 'Departure' }}
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<div
|
||||
@@ -429,13 +429,13 @@
|
||||
class="flex items-center gap-2 text-sm text-white/60"
|
||||
>
|
||||
<v-progress-circular indeterminate size="20" color="cyan" />
|
||||
<span>Lade Frequenzen vom Netzwerk…</span>
|
||||
<span>Loading frequencies from the network…</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="airportFrequencies.length === 0"
|
||||
class="text-xs text-white/50"
|
||||
>
|
||||
Keine Frequenzen verfügbar. Bitte später erneut versuchen.
|
||||
No frequencies available. Please try again later.
|
||||
</div>
|
||||
<div v-else class="space-y-3">
|
||||
<div
|
||||
@@ -527,7 +527,7 @@
|
||||
@click:append-inner="sendPilotText"
|
||||
/>
|
||||
<p class="text-xs text-white/50">
|
||||
Für Notfälle wenn PTT nicht funktioniert oder für Tests
|
||||
For emergencies when PTT fails or for testing.
|
||||
</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -546,7 +546,7 @@
|
||||
</v-chip>
|
||||
</div>
|
||||
<v-chip size="small" :color="simulationRunning ? 'orange' : 'grey'" variant="tonal">
|
||||
{{ simulationRunning ? 'Läuft' : 'Bereit' }}
|
||||
{{ simulationRunning ? 'Running' : 'Ready' }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
@@ -564,7 +564,7 @@
|
||||
v-if="simulationRunning && simulationTrace.length === 0"
|
||||
class="text-sm text-white/60"
|
||||
>
|
||||
Simulation initialisiert...
|
||||
Simulation initializing…
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -602,7 +602,7 @@
|
||||
</div>
|
||||
|
||||
<div class="space-y-2 rounded-2xl border border-white/10 bg-black/30 p-3">
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Aktueller Knoten</p>
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Current node</p>
|
||||
<p class="font-mono text-sm text-white">{{ debugState?.id || '—' }}</p>
|
||||
<p class="text-[11px] text-white/50">
|
||||
{{ debugState ? `${debugState.role} • ${debugState.phase}` : 'N/A' }}
|
||||
@@ -612,12 +612,12 @@
|
||||
Auto: <span class="font-mono text-white">{{ debugState.sayPlain }}</span>
|
||||
</p>
|
||||
<p v-if="debugState?.sayNormalized" class="text-[11px] text-white/40">
|
||||
Funk: {{ debugState.sayNormalized }}
|
||||
Radio: {{ debugState.sayNormalized }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-white/40 mb-2">Nächste Entscheidungen</p>
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-white/40 mb-2">Next decisions</p>
|
||||
<div v-if="debugNextStates.length" class="space-y-2">
|
||||
<div
|
||||
v-for="state in debugNextStates"
|
||||
@@ -646,11 +646,11 @@
|
||||
ATC: <span class="font-mono text-white">{{ state.sayPlain }}</span>
|
||||
</p>
|
||||
<p v-if="state.sayNormalized" class="text-[11px] text-white/40">
|
||||
Funk: {{ state.sayNormalized }}
|
||||
Radio: {{ state.sayNormalized }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="text-xs text-white/50">Keine weiteren Entscheidungen verfügbar.</p>
|
||||
<p v-else class="text-xs text-white/50">No further decisions available.</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
@@ -671,7 +671,7 @@
|
||||
:disabled="log.length === 0"
|
||||
@click="clearLog"
|
||||
>
|
||||
Leeren
|
||||
Clear
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
@@ -702,7 +702,7 @@
|
||||
</div>
|
||||
|
||||
<p v-if="log.length === 0" class="text-xs text-white/50 text-center py-4">
|
||||
Noch keine Kommunikation aufgezeichnet.
|
||||
No transmissions logged yet.
|
||||
</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
@@ -730,7 +730,7 @@
|
||||
class="text-xs uppercase tracking-[0.3em]"
|
||||
:class="lastTransmissionFaulty ? 'text-red-300' : 'text-cyan-300'"
|
||||
>
|
||||
Letzte Übertragung
|
||||
Last transmission
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
@@ -740,7 +740,7 @@
|
||||
color="red"
|
||||
variant="flat"
|
||||
>
|
||||
Fehlerhaft
|
||||
Faulty
|
||||
</v-chip>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@@ -752,7 +752,7 @@
|
||||
<template #prepend>
|
||||
<v-icon size="16">mdi-alert-circle-outline</v-icon>
|
||||
</template>
|
||||
{{ lastTransmissionFaulty ? 'Fehler bearbeiten' : 'Fehler markieren' }}
|
||||
{{ lastTransmissionFaulty ? 'Review issue' : 'Mark issue' }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="lastTransmissionFaulty"
|
||||
@@ -765,7 +765,7 @@
|
||||
<template #prepend>
|
||||
<v-icon size="16">mdi-restore</v-icon>
|
||||
</template>
|
||||
Zurücksetzen
|
||||
Reset
|
||||
</v-btn>
|
||||
<v-btn
|
||||
variant="text"
|
||||
@@ -777,7 +777,7 @@
|
||||
<template #prepend>
|
||||
<v-icon size="16">mdi-close</v-icon>
|
||||
</template>
|
||||
Löschen
|
||||
Delete
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
@@ -786,12 +786,12 @@
|
||||
v-if="lastTransmissionFaulty"
|
||||
class="space-y-2 rounded-xl border border-red-400/30 bg-red-500/10 px-3 py-2"
|
||||
>
|
||||
<p class="text-xs uppercase tracking-[0.25em] text-red-200/80">Fehlerbeschreibung</p>
|
||||
<p class="text-xs uppercase tracking-[0.25em] text-red-200/80">Issue description</p>
|
||||
<p v-if="lastTransmissionFaultNote" class="text-sm text-red-100">
|
||||
{{ lastTransmissionFaultNote }}
|
||||
</p>
|
||||
<p v-else class="text-xs text-red-200/70">
|
||||
Als fehlerhaft markiert.
|
||||
Marked as faulty.
|
||||
</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
@@ -801,16 +801,16 @@
|
||||
<v-card class="bg-[#0b101d] border border-white/10 text-white">
|
||||
<v-card-title class="flex items-center gap-2 text-base font-semibold">
|
||||
<v-icon icon="mdi-alert-circle-outline" color="#f87171" size="20" />
|
||||
Übertragung als fehlerhaft markieren
|
||||
Flag transmission as faulty
|
||||
</v-card-title>
|
||||
<v-card-text class="space-y-4 text-sm text-white/70">
|
||||
<p>
|
||||
Hinterlasse optional eine kurze Beschreibung, damit das Dev Team die
|
||||
Übertragung nachvollziehen kann.
|
||||
Leave an optional short description so the dev team can trace the
|
||||
transmission.
|
||||
</p>
|
||||
<v-textarea
|
||||
v-model="transmissionIssueNote"
|
||||
label="Fehlerbeschreibung (optional)"
|
||||
label="Issue description (optional)"
|
||||
variant="outlined"
|
||||
color="red"
|
||||
rows="3"
|
||||
@@ -819,7 +819,7 @@
|
||||
</v-card-text>
|
||||
<v-card-actions class="justify-end gap-2">
|
||||
<v-btn variant="text" color="grey" @click="cancelTransmissionIssue">
|
||||
Abbrechen
|
||||
Cancel
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="lastTransmissionFaulty"
|
||||
@@ -827,10 +827,10 @@
|
||||
color="cyan"
|
||||
@click="removeTransmissionIssue"
|
||||
>
|
||||
Markierung entfernen
|
||||
Remove flag
|
||||
</v-btn>
|
||||
<v-btn color="red" variant="flat" @click="confirmTransmissionIssue">
|
||||
Speichern
|
||||
Save
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
@@ -844,7 +844,7 @@
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="text-xs uppercase tracking-[0.3em] text-white/40 block mb-2">
|
||||
Signal Stärke
|
||||
Signal strength
|
||||
</label>
|
||||
<v-slider
|
||||
v-model="signalStrength"
|
||||
@@ -863,14 +863,14 @@
|
||||
v-model="radioEffectsEnabled"
|
||||
color="cyan"
|
||||
inset
|
||||
label="Radio-Effekte"
|
||||
label="Radio effects"
|
||||
hide-details
|
||||
/>
|
||||
<v-switch
|
||||
v-model="readbackEnabled"
|
||||
color="cyan"
|
||||
inset
|
||||
label="Readback Stimme"
|
||||
label="Readback voice"
|
||||
hide-details
|
||||
/>
|
||||
<v-switch
|
||||
@@ -1579,11 +1579,11 @@ const loadFlightPlans = async () => {
|
||||
flightPlans.value = response.slice(0, 10)
|
||||
currentScreen.value = 'flightselect'
|
||||
} else {
|
||||
error.value = 'Keine Flugpläne für diese VATSIM ID gefunden'
|
||||
error.value = 'No flight plans found for this VATSIM ID'
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading flight plans:', err)
|
||||
error.value = 'Fehler beim Laden der Flugpläne. Bitte VATSIM ID prüfen.'
|
||||
error.value = 'Error loading flight plans. Please check the VATSIM ID.'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -1947,7 +1947,7 @@ const playAtisBroadcast = async () => {
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
setLastTransmission('ATIS: Keine aktuellen Informationen verfügbar')
|
||||
setLastTransmission('ATIS: No current information available')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user