Simplify bridge onboarding pages

This commit is contained in:
Remi
2025-09-24 00:23:22 +02:00
parent e3c76f452c
commit df9a5dd2f0
2 changed files with 414 additions and 306 deletions

View File

@@ -3,251 +3,283 @@
<div class="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(22,187,215,0.12),transparent_55%)]"/>
<div class="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_bottom,_rgba(79,70,229,0.12),transparent_60%)]"/>
<div class="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col justify-center px-6 py-16 lg:px-12">
<div class="space-y-10">
<header class="text-center">
<p class="text-xs font-semibold uppercase tracking-[0.45em] text-[#16BBD7]">OpenSquawk Bridge</p>
<h1 class="mt-4 text-3xl font-semibold sm:text-4xl">Bridge mit deinem Account verknüpfen</h1>
<p class="mx-auto mt-4 max-w-3xl text-sm text-white/70">
Dieser Bildschirm verbindet den lokal erzeugten Bridge-Token mit deinem OpenSquawk Account. Nach erfolgreicher
Verknüpfung kannst du in die Bridge App zurückkehren die Anwendung erkennt dich dann automatisch.
</p>
</header>
<div class="flex justify-center">
<NuxtLink
to="/bridge"
class="inline-flex items-center gap-2 rounded-full bg-[#292D3B] px-5 py-2.5 text-sm font-medium text-white/80 transition hover:bg-[#353a4c] hover:text-white"
>
<span aria-hidden="true"></span>
Zur Bridge Übersicht
</NuxtLink>
</div>
<div
v-if="!hasToken"
class="rounded-3xl border border-red-500/30 bg-red-500/10 px-8 py-12 text-center shadow-[0_25px_80px_rgba(64,18,18,0.35)]"
<div class="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col gap-10 px-6 py-12 lg:px-12">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<NuxtLink
to="/"
class="inline-flex w-full items-center justify-center gap-2 rounded-2xl border border-white/10 bg-white/5 px-4 py-2 text-sm font-medium text-white transition hover:border-white/20 hover:bg-white/10 sm:w-auto"
>
<h2 class="text-2xl font-semibold">Token fehlt</h2>
<p class="mt-4 text-sm text-red-100/80">
Bitte öffne diesen Bildschirm direkt über die Bridge App. Nur so kann der Token automatisch übergeben werden.
</p>
<p class="mt-6 text-xs uppercase tracking-[0.4em] text-red-200/60">Bridge-Link neu starten</p>
</div>
<span aria-hidden="true"></span>
Back to opensquawk.de
</NuxtLink>
<NuxtLink
to="/bridge"
class="inline-flex w-full items-center justify-center gap-2 rounded-2xl bg-[#292D3B] px-4 py-2 text-sm font-semibold text-white transition hover:bg-[#353a4c] sm:w-auto"
>
Bridge landing
</NuxtLink>
</div>
<div v-else class="grid gap-6 lg:grid-cols-[1.35fr_1fr]">
<div class="space-y-6">
<section
class="rounded-3xl border border-white/10 bg-[#111832]/85 p-8 shadow-[0_30px_80px_rgba(5,10,30,0.55)] backdrop-blur"
>
<div class="flex flex-wrap items-center justify-between gap-3">
<h2 class="text-xl font-semibold">Schritt 1 · Anmelden</h2>
<span class="rounded-full bg-[#292D3B] px-3 py-1 text-xs uppercase tracking-[0.32em] text-white/70">Login</span>
<header class="space-y-4 text-center">
<p class="text-xs font-semibold uppercase tracking-[0.45em] text-[#16BBD7]">OpenSquawk Bridge</p>
<h1 class="text-3xl font-semibold sm:text-4xl">Connect your Bridge token</h1>
<p class="mx-auto max-w-2xl text-sm text-white/70">
Sign in, confirm the token from the desktop Bridge app, and then jump straight back into the simulator.
</p>
</header>
<div
v-if="!hasToken"
class="rounded-3xl border border-red-500/30 bg-red-500/10 px-8 py-12 text-center shadow-[0_25px_80px_rgba(64,18,18,0.35)]"
>
<h2 class="text-2xl font-semibold">Token missing</h2>
<p class="mt-4 text-sm text-red-100/80">Open this page from the Bridge app so the token can be passed through automatically.</p>
<p class="mt-6 text-xs uppercase tracking-[0.4em] text-red-200/60">Waiting for desktop link</p>
</div>
<div v-else class="grid gap-6 lg:grid-cols-[1.35fr_1fr]">
<div class="space-y-6">
<section class="rounded-3xl border border-white/10 bg-[#111832]/85 p-8 shadow-[0_30px_80px_rgba(5,10,30,0.55)] backdrop-blur">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-3">
<span class="flex h-11 w-11 items-center justify-center rounded-2xl bg-[#16BBD7]/10 text-[#16BBD7]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="h-5 w-5"
>
<path d="M7 9V7a5 5 0 1110 0v2" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="5" y="9" width="14" height="11" rx="3" ry="3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 14v3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
<h2 class="text-xl font-semibold">Step 1 · Sign in</h2>
</div>
<p class="mt-3 text-sm text-white/70">
Melde dich mit deinen OpenSquawk Zugangsdaten an. Ohne Anmeldung kann der Token nicht verknüpft werden.
<span class="rounded-full bg-[#292D3B] px-3 py-1 text-[11px] uppercase tracking-[0.32em] text-white/70">Login</span>
</div>
<p class="mt-3 text-sm text-white/70">Use your OpenSquawk credentials to unlock the token linking step.</p>
<div
v-if="isAuthenticated"
class="mt-6 rounded-2xl border border-[#16BBD7]/40 bg-[#16BBD7]/10 px-5 py-4 text-sm"
>
<p class="font-medium text-[#16BBD7]">Signed in</p>
<p class="mt-1 text-white/70">
You are currently logged in as
<span class="font-semibold text-white">{{ authDisplayName }}</span>.
</p>
</div>
<form v-else class="mt-6 space-y-5" @submit.prevent="submitLogin">
<div>
<label class="block text-sm font-medium text-white/70">Email</label>
<input
v-model.trim="loginForm.email"
type="email"
required
autocomplete="email"
class="mt-2 w-full rounded-2xl border border-white/10 bg-[#0B132A]/90 px-4 py-3 text-sm text-white placeholder-white/40 outline-none transition focus:border-[#16BBD7]/60 focus:ring-2 focus:ring-[#16BBD7]/60"
/>
</div>
<div>
<div class="flex items-center justify-between">
<label class="block text-sm font-medium text-white/70">Password</label>
<NuxtLink
to="/forgot-password"
class="text-xs font-semibold uppercase tracking-[0.28em] text-[#16BBD7] transition hover:text-[#72d9ea]"
>
Forgot?
</NuxtLink>
</div>
<input
v-model="loginForm.password"
type="password"
required
autocomplete="current-password"
class="mt-2 w-full rounded-2xl border border-white/10 bg-[#0B132A]/90 px-4 py-3 text-sm text-white placeholder-white/40 outline-none transition focus:border-[#16BBD7]/60 focus:ring-2 focus:ring-[#16BBD7]/60"
/>
</div>
<button
type="submit"
class="inline-flex w-full items-center justify-center gap-2 rounded-2xl bg-[#16BBD7] px-5 py-3 text-sm font-semibold text-[#0B1020] transition hover:bg-[#13a7c4] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#72d9ea] disabled:cursor-not-allowed disabled:bg-[#16BBD7]/60"
:disabled="loginLoading"
>
<span v-if="loginLoading" class="flex items-center gap-2">
<span class="h-4 w-4 animate-spin rounded-full border-2 border-[#0B1020]/30 border-t-[#0B1020]"/>
Signing in
</span>
<span v-else>Sign in</span>
</button>
<p v-if="loginError" class="text-sm text-red-300">{{ loginError }}</p>
</form>
</section>
<section class="rounded-3xl border border-white/10 bg-[#111832]/85 p-8 shadow-[0_30px_80px_rgba(5,10,30,0.55)] backdrop-blur">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center gap-3">
<span class="flex h-11 w-11 items-center justify-center rounded-2xl bg-[#16BBD7]/10 text-[#16BBD7]">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="h-5 w-5"
>
<path d="M8 12l-3 3 3 3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 12l3 3-3 3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9 15h6" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 9A4.5 4.5 0 0112 4.5h0A4.5 4.5 0 0116.5 9" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
<h2 class="text-xl font-semibold">Step 2 · Link the token</h2>
</div>
<span class="rounded-full bg-[#16BBD7]/10 px-3 py-1 text-[11px] uppercase tracking-[0.32em] text-[#16BBD7]">Active</span>
</div>
<p class="mt-3 text-sm text-white/70">Confirm the token that the desktop Bridge generated for you.</p>
<div class="mt-6 flex flex-wrap items-center gap-3">
<code class="rounded-2xl bg-[#0B132A]/80 px-4 py-2 font-mono text-sm text-[#72d9ea] shadow-inner">{{ token }}</code>
<button
type="button"
class="inline-flex items-center gap-2 rounded-2xl border border-white/15 bg-white/5 px-4 py-2 text-xs font-medium text-white/70 transition hover:border-white/30 hover:text-white"
@click="copyToken"
>
<span aria-hidden="true"></span>
<span>{{ copiedToken ? 'Copied' : 'Copy token' }}</span>
</button>
</div>
<div class="mt-8 space-y-4">
<button
type="button"
class="inline-flex w-full items-center justify-center gap-2 rounded-2xl bg-[#16BBD7] px-5 py-3 text-sm font-semibold text-[#0B1020] transition hover:bg-[#13a7c4] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#72d9ea] disabled:cursor-not-allowed disabled:bg-[#16BBD7]/60"
:disabled="!isAuthenticated || connectLoading"
@click="connectBridge"
>
<span v-if="connectLoading" class="flex items-center gap-2">
<span class="h-4 w-4 animate-spin rounded-full border-2 border-[#0B1020]/30 border-t-[#0B1020]"/>
Linking
</span>
<span v-else>Link token</span>
</button>
<p v-if="!isAuthenticated" class="text-sm text-white/60">Sign in first to enable the link button.</p>
<p v-if="connectError" class="text-sm text-red-300">{{ connectError }}</p>
<div
v-if="isAuthenticated"
class="mt-6 rounded-2xl border border-[#16BBD7]/40 bg-[#16BBD7]/10 px-5 py-4 text-sm"
v-if="successBannerVisible"
class="rounded-2xl border border-emerald-400/40 bg-emerald-400/10 px-5 py-4 text-sm text-emerald-100 shadow-[0_0_35px_rgba(16,185,129,0.25)]"
>
<p class="font-medium text-[#16BBD7]">Bereits angemeldet</p>
<p class="mt-1 text-white/70">
Du bist aktuell als
<span class="font-semibold text-white">{{ authDisplayName }}</span>
angemeldet.
</p>
<p class="font-semibold">Linked successfully!</p>
<p class="mt-1 text-emerald-100/80">You can close this tab and return to the Bridge app.</p>
</div>
<form v-else class="mt-6 space-y-5" @submit.prevent="submitLogin">
<div>
<label class="block text-sm font-medium text-white/70">E-Mail</label>
<input
v-model.trim="loginForm.email"
type="email"
required
autocomplete="email"
class="mt-2 w-full rounded-2xl border border-white/10 bg-[#0B132A]/90 px-4 py-3 text-sm text-white placeholder-white/40 outline-none transition focus:border-[#16BBD7]/60 focus:ring-2 focus:ring-[#16BBD7]/60"
/>
</div>
<div>
<div class="flex items-center justify-between">
<label class="block text-sm font-medium text-white/70">Passwort</label>
<NuxtLink
to="/forgot-password"
class="text-xs font-semibold uppercase tracking-[0.28em] text-[#16BBD7] transition hover:text-[#72d9ea]"
>
Passwort vergessen?
</NuxtLink>
</div>
<input
v-model="loginForm.password"
type="password"
required
autocomplete="current-password"
class="mt-2 w-full rounded-2xl border border-white/10 bg-[#0B132A]/90 px-4 py-3 text-sm text-white placeholder-white/40 outline-none transition focus:border-[#16BBD7]/60 focus:ring-2 focus:ring-[#16BBD7]/60"
/>
</div>
<button
type="submit"
class="inline-flex w-full items-center justify-center gap-2 rounded-2xl bg-[#16BBD7] px-5 py-3 text-sm font-semibold text-[#0B1020] transition hover:bg-[#13a7c4] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#72d9ea] disabled:cursor-not-allowed disabled:bg-[#16BBD7]/60"
:disabled="loginLoading"
>
<span v-if="loginLoading" class="flex items-center gap-2">
<span class="h-4 w-4 animate-spin rounded-full border-2 border-[#0B1020]/30 border-t-[#0B1020]"/>
Anmeldung läuft
</span>
<span v-else>Anmelden</span>
</button>
<p v-if="loginError" class="text-sm text-red-300">{{ loginError }}</p>
</form>
</section>
<section
class="rounded-3xl border border-white/10 bg-[#111832]/85 p-8 shadow-[0_30px_80px_rgba(5,10,30,0.55)] backdrop-blur"
>
<div class="flex flex-wrap items-center justify-between gap-3">
<h2 class="text-xl font-semibold">Schritt 2 · Bridge verknüpfen</h2>
<span class="rounded-full bg-[#16BBD7]/10 px-3 py-1 text-xs uppercase tracking-[0.32em] text-[#16BBD7]">
Token aktiv
</span>
</div>
<p class="mt-3 text-sm text-white/70">
Der unten stehende Token stammt direkt aus deiner Bridge App. Verknüpfe ihn mit einem Klick mit deinem Account.
</p>
<div class="mt-6 flex flex-wrap items-center gap-3">
<code class="rounded-2xl bg-[#0B132A]/80 px-4 py-2 font-mono text-sm text-[#72d9ea] shadow-inner">
{{ token }}
</code>
<button
type="button"
class="inline-flex items-center gap-2 rounded-2xl border border-white/15 bg-white/5 px-4 py-2 text-xs font-medium text-white/70 transition hover:border-white/30 hover:text-white"
@click="copyToken"
>
<span aria-hidden="true"></span>
<span>{{ copiedToken ? 'Token kopiert' : 'Token kopieren' }}</span>
</button>
</div>
<div class="mt-8 space-y-4">
<button
type="button"
class="inline-flex w-full items-center justify-center gap-2 rounded-2xl bg-[#16BBD7] px-5 py-3 text-sm font-semibold text-[#0B1020] transition hover:bg-[#13a7c4] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#72d9ea] disabled:cursor-not-allowed disabled:bg-[#16BBD7]/60"
:disabled="!isAuthenticated || connectLoading"
@click="connectBridge"
>
<span v-if="connectLoading" class="flex items-center gap-2">
<span class="h-4 w-4 animate-spin rounded-full border-2 border-[#0B1020]/30 border-t-[#0B1020]"/>
Token wird verbunden
</span>
<span v-else>Bridge jetzt verbinden</span>
</button>
<p v-if="!isAuthenticated" class="text-sm text-white/60">
Bitte melde dich zuerst an, um die Verknüpfung durchführen zu können.
</p>
<p v-if="connectError" class="text-sm text-red-300">{{ connectError }}</p>
<div
v-if="successBannerVisible"
class="rounded-2xl border border-emerald-400/40 bg-emerald-400/10 px-5 py-4 text-sm text-emerald-100 shadow-[0_0_35px_rgba(16,185,129,0.25)]"
>
<p class="font-semibold">Verbindung hergestellt!</p>
<p class="mt-1 text-emerald-100/80">
Du kannst dieses Fenster jetzt schließen und zur Bridge App zurückkehren. Statusupdates werden automatisch
synchronisiert.
</p>
</div>
</div>
</section>
</div>
<aside>
<section
class="h-full rounded-3xl border border-white/10 bg-[#111832]/80 p-8 shadow-[0_30px_80px_rgba(4,8,24,0.55)] backdrop-blur"
:class="{ 'opacity-80': statusLoading }"
>
<div class="flex flex-wrap items-center justify-between gap-3">
<h3 class="text-lg font-semibold">Live Status</h3>
<span class="rounded-full bg-white/5 px-3 py-1 text-[11px] uppercase tracking-[0.3em] text-white/50">
Update alle 5s
</span>
</div>
<div class="mt-6 space-y-4">
<div class="rounded-2xl border border-white/10 bg-[#0B132A]/80 px-5 py-4 shadow-inner">
<p class="text-sm font-semibold text-white/70">Verbindung</p>
<p
class="mt-1 text-base font-medium"
:class="connectionStatus?.connected ? 'text-emerald-300' : 'text-white/50'"
>
{{ connectionLabel }}
</p>
<p v-if="connectionSubLabel" class="text-xs text-white/40">{{ connectionSubLabel }}</p>
</div>
<div class="grid gap-3 sm:grid-cols-2">
<div
class="flex items-center gap-3 rounded-2xl border px-4 py-3 transition"
:class="connectionStatus?.simConnected
? 'border-emerald-400/60 bg-emerald-400/10 shadow-[0_0_30px_rgba(16,185,129,0.35)]'
: 'border-white/10 bg-[#0B132A]/70'"
>
<span
class="h-3 w-3 rounded-full"
:class="connectionStatus?.simConnected
? 'bg-emerald-400 shadow-[0_0_18px_rgba(16,185,129,0.85)]'
: 'bg-white/30'"
/>
<div>
<p class="text-sm font-semibold">Simulator</p>
<p class="text-xs text-white/60">
{{ connectionStatus?.simConnected ? 'Simulator erkannt' : 'Kein Simulator aktiv' }}
</p>
</div>
</div>
<div
class="flex items-center gap-3 rounded-2xl border px-4 py-3 transition"
:class="connectionStatus?.flightActive
? 'border-emerald-400/60 bg-emerald-400/10 shadow-[0_0_30px_rgba(16,185,129,0.35)]'
: 'border-white/10 bg-[#0B132A]/70'"
>
<span
class="h-3 w-3 rounded-full"
:class="connectionStatus?.flightActive
? 'bg-emerald-400 shadow-[0_0_18px_rgba(16,185,129,0.85)]'
: 'bg-white/30'"
/>
<div>
<p class="text-sm font-semibold">Flugstatus</p>
<p class="text-xs text-white/60">
{{ connectionStatus?.flightActive ? 'Flug aktiv' : 'Kein Flug erkannt' }}
</p>
</div>
</div>
</div>
<div class="rounded-2xl border border-white/10 bg-[#0B132A]/80 px-5 py-4 text-sm text-white/65">
<div class="flex items-center justify-between">
<span>Verbunden seit</span>
<span class="font-medium text-white">{{ formattedConnectedAt }}</span>
</div>
<div class="mt-2 flex items-center justify-between text-white/60">
<span>Letztes Status-Update</span>
<span class="font-medium text-white">{{ formattedLastStatusAt }}</span>
</div>
</div>
</div>
<p v-if="statusLoading" class="mt-6 text-xs uppercase tracking-[0.4em] text-white/45">Status wird aktualisiert </p>
<p v-if="statusError" class="mt-2 text-sm text-red-300">{{ statusError }}</p>
</section>
</aside>
</div>
</section>
</div>
<aside>
<section
class="h-full rounded-3xl border border-white/10 bg-[#111832]/80 p-8 shadow-[0_30px_80px_rgba(4,8,24,0.55)] backdrop-blur"
:class="{ 'opacity-80': statusLoading }"
>
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
<span class="flex h-11 w-11 items-center justify-center rounded-2xl bg-white/5 text-white/70">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="h-5 w-5"
>
<path d="M4 17l4-4 3 3 5-5 4 4" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 7h16" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 12h10" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
<div>
<h3 class="text-lg font-semibold">Live status</h3>
<p class="text-xs uppercase tracking-[0.32em] text-white/50">Refreshes every 5s</p>
</div>
</div>
</div>
<div class="mt-6 space-y-4">
<div class="rounded-2xl border border-white/10 bg-[#0B132A]/80 px-5 py-4 shadow-inner">
<p class="text-sm font-semibold text-white/70">Connection</p>
<p
class="mt-1 text-base font-medium"
:class="connectionStatus?.connected ? 'text-emerald-300' : 'text-white/50'"
>
{{ connectionLabel }}
</p>
<p v-if="connectionSubLabel" class="text-xs text-white/40">{{ connectionSubLabel }}</p>
</div>
<div class="grid gap-3 sm:grid-cols-2">
<div
class="flex items-center gap-3 rounded-2xl border px-4 py-3 transition"
:class="connectionStatus?.simConnected
? 'border-emerald-400/60 bg-emerald-400/10 shadow-[0_0_30px_rgba(16,185,129,0.35)]'
: 'border-white/10 bg-[#0B132A]/70'"
>
<span
class="flex h-3 w-3 items-center justify-center rounded-full"
:class="connectionStatus?.simConnected
? 'bg-emerald-400 shadow-[0_0_18px_rgba(16,185,129,0.85)]'
: 'bg-white/30'"
/>
<div>
<p class="text-sm font-semibold">Simulator</p>
<p class="text-xs text-white/60">
{{ connectionStatus?.simConnected ? 'Simulator detected' : 'Waiting for simulator' }}
</p>
</div>
</div>
<div
class="flex items-center gap-3 rounded-2xl border px-4 py-3 transition"
:class="connectionStatus?.flightActive
? 'border-emerald-400/60 bg-emerald-400/10 shadow-[0_0_30px_rgba(16,185,129,0.35)]'
: 'border-white/10 bg-[#0B132A]/70'"
>
<span
class="flex h-3 w-3 items-center justify-center rounded-full"
:class="connectionStatus?.flightActive
? 'bg-emerald-400 shadow-[0_0_18px_rgba(16,185,129,0.85)]'
: 'bg-white/30'"
/>
<div>
<p class="text-sm font-semibold">Flight</p>
<p class="text-xs text-white/60">
{{ connectionStatus?.flightActive ? 'Flight in progress' : 'No flight detected' }}
</p>
</div>
</div>
</div>
<div class="rounded-2xl border border-white/10 bg-[#0B132A]/80 px-5 py-4 text-sm text-white/65">
<div class="flex items-center justify-between">
<span>Connected since</span>
<span class="font-medium text-white">{{ formattedConnectedAt }}</span>
</div>
<div class="mt-2 flex items-center justify-between text-white/60">
<span>Last update</span>
<span class="font-medium text-white">{{ formattedLastStatusAt }}</span>
</div>
</div>
</div>
<p v-if="statusLoading" class="mt-6 text-xs uppercase tracking-[0.4em] text-white/45">Refreshing </p>
<p v-if="statusError" class="mt-2 text-sm text-red-300">{{ statusError }}</p>
</section>
</aside>
</div>
</div>
</div>
@@ -273,7 +305,7 @@ interface BridgeStatusPayload {
lastStatusAt: string | null
}
useHead({ title: 'Bridge verbinden · OpenSquawk' })
useHead({ title: 'Link Bridge · OpenSquawk' })
const route = useRoute()
const auth = useAuthStore()
@@ -312,10 +344,10 @@ const authDisplayName = computed(() => {
const connectionLabel = computed(() => {
if (!connectionStatus.value?.connected) {
return 'Wartet auf Verbindung'
return 'Waiting for the desktop app'
}
const name = connectionStatus.value.user?.name || connectionStatus.value.user?.email
return name ? `Verbunden als ${name}` : 'Verbunden'
return name ? `Linked as ${name}` : 'Linked'
})
const connectionSubLabel = computed(() => {
@@ -338,7 +370,7 @@ function formatTimestamp(value: string | null) {
if (Number.isNaN(parsed.getTime())) {
return '—'
}
return new Intl.DateTimeFormat('de-DE', {
return new Intl.DateTimeFormat('en-GB', {
dateStyle: 'short',
timeStyle: 'medium',
}).format(parsed)
@@ -348,7 +380,7 @@ async function submitLogin() {
loginError.value = ''
connectError.value = ''
if (!loginForm.email || !loginForm.password) {
loginError.value = 'Bitte E-Mail und Passwort eingeben.'
loginError.value = 'Please enter your email and password.'
return
}
@@ -362,7 +394,7 @@ async function submitLogin() {
err?.data?.statusMessage ||
err?.response?._data?.statusMessage ||
err?.message ||
'Login fehlgeschlagen.'
'Login failed.'
} finally {
loginLoading.value = false
}
@@ -390,7 +422,7 @@ async function connectBridge() {
err?.data?.statusMessage ||
err?.response?._data?.statusMessage ||
err?.message ||
'Verknüpfung fehlgeschlagen.'
'Could not link the token.'
} finally {
connectLoading.value = false
}
@@ -424,7 +456,7 @@ async function fetchStatus(force = false) {
err?.data?.statusMessage ||
err?.response?._data?.statusMessage ||
err?.message ||
'Status konnte nicht geladen werden.'
'We could not refresh the status.'
} finally {
statusRequestActive.value = false
statusLoading.value = false
@@ -441,7 +473,7 @@ async function copyToken() {
copiedToken.value = false
}, 2000)
} catch {
// Ignorieren Kopie ist optional
// Ignore copy errors clipboard access is optional
}
}
@@ -500,4 +532,3 @@ code {
word-break: break-all;
}
</style>

View File

@@ -3,23 +3,36 @@
<div class="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_top,_rgba(22,187,215,0.12),transparent_55%)]"/>
<div class="pointer-events-none absolute inset-0 bg-[radial-gradient(circle_at_bottom,_rgba(14,165,233,0.18),transparent_65%)]"/>
<div class="relative mx-auto w-full max-w-6xl px-6 py-16 lg:px-12">
<header class="text-center">
<p class="text-xs font-semibold uppercase tracking-[0.45em] text-[#16BBD7]">OpenSquawk Bridge</p>
<h1 class="mt-4 text-3xl font-semibold sm:text-4xl">Deine Verbindung zwischen Simulator und OpenSquawk</h1>
<p class="mx-auto mt-4 max-w-3xl text-sm text-white/70">
Die OpenSquawk Bridge streamt Ereignisse aus deinem Simulator direkt zu unserem Netzwerk. Installiere die passende
Version für deine Plattform, starte die App und verknüpfe sie in wenigen Klicks mit deinem Account.
<div class="relative mx-auto w-full max-w-6xl px-6 py-12 lg:px-12">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<NuxtLink
to="/"
class="inline-flex w-full items-center justify-center gap-2 rounded-2xl border border-white/10 bg-white/5 px-4 py-2 text-sm font-medium text-white transition hover:border-white/20 hover:bg-white/10 sm:w-auto"
>
<span aria-hidden="true"></span>
Back to opensquawk.de
</NuxtLink>
<NuxtLink
to="/bridge/connect"
class="inline-flex w-full items-center justify-center gap-2 rounded-2xl bg-[#292D3B] px-4 py-2 text-sm font-semibold text-white transition hover:bg-[#353a4c] sm:w-auto"
>
Already have a token?
</NuxtLink>
</div>
<header class="mt-10 space-y-4 text-center sm:text-left">
<span class="inline-flex items-center gap-2 rounded-full bg-[#16BBD7]/10 px-4 py-2 text-xs font-semibold uppercase tracking-[0.45em] text-[#16BBD7]">Bridge</span>
<h1 class="text-3xl font-semibold sm:text-4xl">Download the OpenSquawk Bridge</h1>
<p class="max-w-3xl text-sm text-white/70">
Pick the build for your simulator and let the desktop Bridge handle the rest. Tokens, login, and live status are all baked in.
</p>
</header>
<div class="mt-12 grid gap-6 lg:grid-cols-[1.15fr_0.85fr]">
<div class="mt-12 grid gap-6 lg:grid-cols-[1.2fr_0.8fr]">
<section class="space-y-6">
<div class="rounded-3xl border border-white/10 bg-[#111832]/80 p-8 shadow-[0_30px_80px_rgba(5,10,30,0.55)] backdrop-blur">
<h2 class="text-xl font-semibold">Download-Zentrale</h2>
<p class="mt-2 text-sm text-white/70">
Wähle die Version deiner Bridge. Neue Simulatoren werden automatisch nachgereicht, sobald Builds verfügbar sind.
</p>
<h2 class="text-xl font-semibold">Downloads</h2>
<p class="mt-2 text-sm text-white/70">Short and sweet. Click, install, fly.</p>
<div class="mt-6 grid gap-5 md:grid-cols-2">
<article
@@ -27,15 +40,58 @@
:key="item.id"
class="flex h-full flex-col justify-between rounded-3xl border border-white/10 bg-[#0B132A]/80 p-6 transition hover:border-white/20 hover:shadow-[0_25px_60px_rgba(12,18,40,0.5)]"
>
<div class="space-y-3">
<span
class="inline-flex items-center gap-2 rounded-full px-3 py-1 text-[11px] uppercase tracking-[0.35em]"
:class="item.available ? 'bg-[#16BBD7]/20 text-[#16BBD7]' : 'bg-white/10 text-white/60'"
>
{{ item.badge }}
<div class="space-y-4">
<span class="flex h-12 w-12 items-center justify-center rounded-2xl bg-white/5 text-[#16BBD7]">
<svg
v-if="item.icon === 'plane'"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="h-6 w-6"
>
<path d="M2 13l10-3 10 3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 10V3" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 14v7" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 21l5-7 5 7" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg
v-else-if="item.icon === 'future'"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="h-6 w-6"
>
<path d="M12 3l3 6 6 1-4.5 4.3L18 21l-6-3-6 3 1.5-6.7L3 10l6-1z" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="h-6 w-6"
>
<path d="M4 17l8-11 8 11" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 7h16" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M4 12h16" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</span>
<h3 class="text-lg font-semibold">{{ item.title }}</h3>
<p class="text-sm text-white/65">{{ item.description }}</p>
<div class="space-y-2">
<span
class="inline-flex items-center gap-2 rounded-full px-3 py-1 text-[11px] uppercase tracking-[0.35em]"
:class="item.available ? 'bg-[#16BBD7]/20 text-[#16BBD7]' : 'bg-white/10 text-white/60'"
>
{{ item.badge }}
</span>
<h3 class="text-lg font-semibold">{{ item.title }}</h3>
<p class="text-sm text-white/65">{{ item.description }}</p>
</div>
</div>
<div class="mt-6">
@@ -47,7 +103,7 @@
class="inline-flex w-full items-center justify-center gap-2 rounded-2xl bg-[#16BBD7] px-4 py-3 text-sm font-semibold text-[#0B1020] transition hover:bg-[#13a7c4] focus:outline-none focus-visible:ring-2 focus-visible:ring-[#72d9ea]"
>
<span aria-hidden="true"></span>
Download starten
Download
</a>
<button
v-else
@@ -64,57 +120,58 @@
</div>
<div class="rounded-3xl border border-white/10 bg-[#111832]/70 p-8 shadow-[0_20px_60px_rgba(4,8,24,0.45)] backdrop-blur">
<h2 class="text-xl font-semibold">So funktioniert die Verknüpfung</h2>
<h2 class="text-xl font-semibold">How it works</h2>
<ol class="mt-6 space-y-4 text-sm text-white/70">
<li class="flex items-start gap-4">
<span class="mt-1 flex h-8 w-8 items-center justify-center rounded-full bg-[#16BBD7]/20 text-[#16BBD7]">1</span>
<span>Bridge herunterladen, installieren und starten. Die App erzeugt automatisch deinen Token.</span>
</li>
<li class="flex items-start gap-4">
<span class="mt-1 flex h-8 w-8 items-center justify-center rounded-full bg-[#16BBD7]/20 text-[#16BBD7]">2</span>
<span>Folge dem angezeigten Link <code class="rounded bg-[#0B132A]/80 px-2 py-1 text-xs">opensquawk.de/bridge/connect</code>
der Token wird beim Öffnen automatisch übergeben.</span>
</li>
<li class="flex items-start gap-4">
<span class="mt-1 flex h-8 w-8 items-center justify-center rounded-full bg-[#16BBD7]/20 text-[#16BBD7]">3</span>
<span>Melde dich mit deinem OpenSquawk Account an, bestätige die Verknüpfung und kehre zur App zurück.</span>
<li v-for="step in steps" :key="step.id" class="flex items-start gap-4">
<span class="mt-1 flex h-8 w-8 items-center justify-center rounded-full bg-[#16BBD7]/20 text-[#16BBD7]">{{ step.id }}</span>
<div>
<p class="font-medium text-white">{{ step.title }}</p>
<p class="text-white/70">{{ step.description }}</p>
</div>
</li>
</ol>
<div class="mt-6 rounded-2xl border border-white/10 bg-[#0B132A]/70 px-5 py-4 text-sm text-white/70">
<p class="font-semibold text-white">Hinweis</p>
<p class="mt-1 text-white/65">
Die Bridge zeigt dir jederzeit an, ob der Simulator erkannt wurde und ob ein Flug aktiv ist. Alle Statusmeldungen
werden alle zehn Sekunden an OpenSquawk übertragen.
</p>
<p class="font-semibold text-white">Status at a glance</p>
<p class="mt-1 text-white/65">The desktop Bridge mirrors your login, shows simulator + flight status, and updates OpenSquawk every few seconds.</p>
</div>
</div>
</section>
<aside class="space-y-6">
<div class="rounded-3xl border border-white/10 bg-[#111832]/80 p-8 shadow-[0_30px_80px_rgba(4,8,24,0.55)] backdrop-blur">
<h2 class="text-xl font-semibold">Systemvoraussetzungen</h2>
<h2 class="text-xl font-semibold">Requirements</h2>
<ul class="mt-4 space-y-3 text-sm text-white/70">
<li>Windows 10/11 (64-bit)</li>
<li>.NET 8 Desktop Runtime</li>
<li>Aktive OpenSquawk Mitgliedschaft</li>
<li>Internetverbindung (mind. 5 Mbit/s)</li>
<li class="flex items-center gap-3">
<span class="h-2 w-2 rounded-full bg-[#16BBD7] shadow-[0_0_12px_rgba(22,187,215,0.8)]"/>
Windows 10/11 (64-bit)
</li>
<li class="flex items-center gap-3">
<span class="h-2 w-2 rounded-full bg-[#16BBD7] shadow-[0_0_12px_rgba(22,187,215,0.8)]"/>
.NET 8 Desktop Runtime
</li>
<li class="flex items-center gap-3">
<span class="h-2 w-2 rounded-full bg-[#16BBD7] shadow-[0_0_12px_rgba(22,187,215,0.8)]"/>
Active OpenSquawk membership
</li>
<li class="flex items-center gap-3">
<span class="h-2 w-2 rounded-full bg-[#16BBD7] shadow-[0_0_12px_rgba(22,187,215,0.8)]"/>
Reliable internet connection
</li>
</ul>
</div>
<div class="rounded-3xl border border-white/10 bg-[#111832]/70 p-8 shadow-[0_20px_60px_rgba(4,8,24,0.45)] backdrop-blur">
<h2 class="text-xl font-semibold">Support</h2>
<h2 class="text-xl font-semibold">Need help?</h2>
<p class="mt-3 text-sm text-white/70">
Fragen oder Feedback? Unser Team hilft dir gerne weiter. Schreibe uns jederzeit an
<a href="mailto:support@opensquawk.de" class="text-[#16BBD7] underline decoration-dotted underline-offset-4">support@opensquawk.de</a>.
Ping us at
<a href="mailto:support@opensquawk.de" class="text-[#16BBD7] underline decoration-dotted underline-offset-4">support@opensquawk.de</a>
and we'll get you flying again.
</p>
<div class="mt-6 rounded-2xl border border-[#16BBD7]/30 bg-[#16BBD7]/10 px-5 py-4 text-sm text-white/75">
<p class="font-semibold text-[#16BBD7]">Pro Tipp</p>
<p class="mt-1">
Lass die Bridge App im Hintergrund laufen sie synchronisiert automatisch, sobald dein Simulator startet und ein
Flug aktiv ist.
</p>
<p class="font-semibold text-[#16BBD7]">Tip</p>
<p class="mt-1">Leave the Bridge running in the background. It wakes up when the simulator launches and keeps the data fresh.</p>
</div>
</div>
</aside>
@@ -126,32 +183,53 @@
<script setup lang="ts">
import { useHead } from '#imports'
useHead({ title: 'Bridge · OpenSquawk' })
useHead({ title: 'Bridge Downloads · OpenSquawk' })
const downloads = [
{
id: 'msfs2020',
title: 'Microsoft Flight Simulator 2020',
description: 'Stabile Bridge-Version für den MSFS 2020 mit automatischer Token-Verknüpfung und Status-Updates.',
badge: 'Verfügbar',
description: 'Stable build with automatic token linking and live updates.',
badge: 'Available now',
available: true,
href: 'https://github.com/itsrubberduck/OpenSquawk-MSFS-Bridge/',
icon: 'plane',
},
{
id: 'msfs2024',
title: 'Microsoft Flight Simulator 2024',
description: 'Die neue Generation des MSFS wird aktuell angebunden. Release der Bridge folgt in Kürze.',
badge: 'In Arbeit',
description: 'Integration in progress. The release build is almost ready.',
badge: 'In progress',
available: false,
href: '#',
icon: 'future',
},
{
id: 'xplane',
title: 'X-Plane 12',
description: 'Gemeinsame Bridge-Architektur X-Plane Support ist geplant, sobald die MSFS-Versionen stabil laufen.',
badge: 'In Planung',
description: 'Shared Bridge architecture. Support arrives later this year.',
badge: 'Planned',
available: false,
href: '#',
icon: 'xplane',
},
]
const steps = [
{
id: 1,
title: 'Install the Bridge',
description: 'Download the build, run the installer, and launch the desktop app.',
},
{
id: 2,
title: 'Open the connect page',
description: 'Follow the in-app link to opensquawk.de/bridge/connect. The token comes along automatically.',
},
{
id: 3,
title: 'Link and fly',
description: 'Sign in, link the token, and head back to the simulator. The Bridge keeps sending updates in the background.',
},
]
</script>
@@ -161,4 +239,3 @@ code {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
</style>