mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-05-14 19:25:37 +08:00
change footer linsk
This commit is contained in:
@@ -43,7 +43,8 @@
|
|||||||
v-if="option.id === activeExperience.id"
|
v-if="option.id === activeExperience.id"
|
||||||
size="16"
|
size="16"
|
||||||
class="experience-option-check"
|
class="experience-option-check"
|
||||||
>mdi-check</v-icon>
|
>mdi-check
|
||||||
|
</v-icon>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
@@ -81,7 +82,9 @@
|
|||||||
aria-label="Lesson search results"
|
aria-label="Lesson search results"
|
||||||
>
|
>
|
||||||
<div class="lesson-search-header">
|
<div class="lesson-search-header">
|
||||||
<span>{{ lessonSearchResults.length }} {{ lessonSearchResults.length === 1 ? 'match' : 'matches' }}</span>
|
<span>{{ lessonSearchResults.length }} {{
|
||||||
|
lessonSearchResults.length === 1 ? 'match' : 'matches'
|
||||||
|
}}</span>
|
||||||
<button class="link small" type="button" @click="clearLessonSearch">Clear</button>
|
<button class="link small" type="button" @click="clearLessonSearch">Clear</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="lessonSearchResults.length" class="lesson-search-groups" role="presentation">
|
<div v-if="lessonSearchResults.length" class="lesson-search-groups" role="presentation">
|
||||||
@@ -94,7 +97,10 @@
|
|||||||
>
|
>
|
||||||
<div class="lesson-search-group-header">
|
<div class="lesson-search-group-header">
|
||||||
<div class="lesson-search-group-title">{{ group.module.title }}</div>
|
<div class="lesson-search-group-title">{{ group.module.title }}</div>
|
||||||
<div v-if="group.module.subtitle" class="lesson-search-group-sub">{{ group.module.subtitle }}</div>
|
<div v-if="group.module.subtitle" class="lesson-search-group-sub">{{
|
||||||
|
group.module.subtitle
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
v-for="hit in group.hits"
|
v-for="hit in group.hits"
|
||||||
@@ -106,9 +112,13 @@
|
|||||||
>
|
>
|
||||||
<div class="lesson-search-item-text">
|
<div class="lesson-search-item-text">
|
||||||
<div class="lesson-search-item-title">{{ hit.lesson.title }}</div>
|
<div class="lesson-search-item-title">{{ hit.lesson.title }}</div>
|
||||||
<div class="lesson-search-item-desc">{{ hit.lesson.desc || 'Practice radio calls with ATC' }}</div>
|
<div class="lesson-search-item-desc">{{
|
||||||
|
hit.lesson.desc || 'Practice radio calls with ATC'
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="lesson-score lesson-search-item-score" :class="lessonScoreClass(hit.module.id, hit.lesson.id)">
|
<span class="lesson-score lesson-search-item-score"
|
||||||
|
:class="lessonScoreClass(hit.module.id, hit.lesson.id)">
|
||||||
<v-icon size="14">{{ lessonScoreIcon(hit.module.id, hit.lesson.id) }}</v-icon>
|
<v-icon size="14">{{ lessonScoreIcon(hit.module.id, hit.lesson.id) }}</v-icon>
|
||||||
{{ lessonScoreLabel(hit.module.id, hit.lesson.id) }}
|
{{ lessonScoreLabel(hit.module.id, hit.lesson.id) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -142,7 +152,6 @@
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- HUB -->
|
<!-- HUB -->
|
||||||
<main v-if="panel==='hub'" class="container" role="main">
|
<main v-if="panel==='hub'" class="container" role="main">
|
||||||
<div class="hub-head">
|
<div class="hub-head">
|
||||||
@@ -204,7 +213,8 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="tile-overlay-link"
|
class="tile-overlay-link"
|
||||||
@click.stop.prevent="attemptUnlockModule(m.id)"
|
@click.stop.prevent="attemptUnlockModule(m.id)"
|
||||||
>unlock</button>
|
>unlock
|
||||||
|
</button>
|
||||||
this briefing.
|
this briefing.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,11 +245,13 @@
|
|||||||
<div class="play-tools">
|
<div class="play-tools">
|
||||||
<div v-if="requiresFlightPlan" class="plan-status" :class="{ 'is-ready': !!currentPlan }">
|
<div v-if="requiresFlightPlan" class="plan-status" :class="{ 'is-ready': !!currentPlan }">
|
||||||
<div class="plan-status-icon">
|
<div class="plan-status-icon">
|
||||||
<v-icon :icon="currentPlan ? 'mdi-check-circle-outline' : 'mdi-alert-circle-outline'" size="22" />
|
<v-icon :icon="currentPlan ? 'mdi-check-circle-outline' : 'mdi-alert-circle-outline'" size="22"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="plan-status-body">
|
<div class="plan-status-body">
|
||||||
<span class="plan-status-title">
|
<span class="plan-status-title">
|
||||||
{{ currentPlan ? (currentPlan.scenario.callsign || currentPlan.scenario.radioCall) : 'Flight plan pending' }}
|
{{
|
||||||
|
currentPlan ? (currentPlan.scenario.callsign || currentPlan.scenario.radioCall) : 'Flight plan pending'
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span class="plan-status-sub" v-if="currentPlan">{{ currentPlanRoute }}</span>
|
<span class="plan-status-sub" v-if="currentPlan">{{ currentPlanRoute }}</span>
|
||||||
<span class="plan-status-sub muted" v-else>Select or import a flight to launch.</span>
|
<span class="plan-status-sub muted" v-else>Select or import a flight to launch.</span>
|
||||||
@@ -256,7 +268,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="moduleStage==='lessons'" class="stats">
|
<div v-if="moduleStage==='lessons'" class="stats">
|
||||||
<span class="stat"><v-icon size="18">mdi-check-circle-outline</v-icon> {{ doneCount(current.id) }}/{{ current.lessons.length }}</span>
|
<span class="stat"><v-icon size="18">mdi-check-circle-outline</v-icon> {{
|
||||||
|
doneCount(current.id)
|
||||||
|
}}/{{ current.lessons.length }}</span>
|
||||||
<span class="stat"><v-icon size="18">mdi-star</v-icon> Ø {{ avgScore(current.id) }}%</span>
|
<span class="stat"><v-icon size="18">mdi-star</v-icon> Ø {{ avgScore(current.id) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -289,7 +303,7 @@
|
|||||||
|
|
||||||
<div v-if="flightPlanMode==='random'" class="plan-panel">
|
<div v-if="flightPlanMode==='random'" class="plan-panel">
|
||||||
<div class="plan-summary">
|
<div class="plan-summary">
|
||||||
<NuxtImg :src="currentBriefingArt" alt="Mission hero" class="plan-hero" />
|
<NuxtImg :src="currentBriefingArt" alt="Mission hero" class="plan-hero"/>
|
||||||
<div class="plan-summary-body">
|
<div class="plan-summary-body">
|
||||||
<span class="plan-tag">Auto flight</span>
|
<span class="plan-tag">Auto flight</span>
|
||||||
<div class="plan-callout">{{ displayCallsign(draftPlanScenario?.radioCall, draftPlanScenario) }}</div>
|
<div class="plan-callout">{{ displayCallsign(draftPlanScenario?.radioCall, draftPlanScenario) }}</div>
|
||||||
@@ -309,7 +323,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form v-else-if="flightPlanMode==='manual'" class="plan-panel manual-panel" @submit.prevent="handleManualSubmit">
|
<form v-else-if="flightPlanMode==='manual'" class="plan-panel manual-panel"
|
||||||
|
@submit.prevent="handleManualSubmit">
|
||||||
<div class="manual-grid">
|
<div class="manual-grid">
|
||||||
<div class="manual-form">
|
<div class="manual-form">
|
||||||
<div class="manual-card manual-card--intro">
|
<div class="manual-card manual-card--intro">
|
||||||
@@ -319,7 +334,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="manual-card-title">Design your own mission</div>
|
<div class="manual-card-title">Design your own mission</div>
|
||||||
<p class="muted small">Fill in the essentials below. Expand the optional sections when you want to brief gates, taxi routes or procedures.</p>
|
<p class="muted small">Fill in the essentials below. Expand the optional sections when you want to
|
||||||
|
brief gates, taxi routes or procedures.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -331,29 +347,30 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="manual-card-title">Core flight data</div>
|
<div class="manual-card-title">Core flight data</div>
|
||||||
<p class="muted small">We only need a callsign plus departure and destination to spin up a training scenario.</p>
|
<p class="muted small">We only need a callsign plus departure and destination to spin up a training
|
||||||
|
scenario.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-grid required-grid">
|
<div class="field-grid required-grid">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Airline ICAO<span class="required-dot" aria-hidden="true">•</span></span>
|
<span>Airline ICAO<span class="required-dot" aria-hidden="true">•</span></span>
|
||||||
<input v-model="manualForm.airlineCode" maxlength="4" placeholder="DLH" />
|
<input v-model="manualForm.airlineCode" maxlength="4" placeholder="DLH"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Flight number<span class="required-dot" aria-hidden="true">•</span></span>
|
<span>Flight number<span class="required-dot" aria-hidden="true">•</span></span>
|
||||||
<input v-model="manualForm.flightNumber" placeholder="400" />
|
<input v-model="manualForm.flightNumber" placeholder="400"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Spoken callsign</span>
|
<span>Spoken callsign</span>
|
||||||
<input v-model="manualForm.airlineCall" placeholder="Lufthansa" />
|
<input v-model="manualForm.airlineCall" placeholder="Lufthansa"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Departure ICAO<span class="required-dot" aria-hidden="true">•</span></span>
|
<span>Departure ICAO<span class="required-dot" aria-hidden="true">•</span></span>
|
||||||
<input v-model="manualForm.departureIcao" placeholder="EDDF" maxlength="4" />
|
<input v-model="manualForm.departureIcao" placeholder="EDDF" maxlength="4"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Destination ICAO<span class="required-dot" aria-hidden="true">•</span></span>
|
<span>Destination ICAO<span class="required-dot" aria-hidden="true">•</span></span>
|
||||||
<input v-model="manualForm.destinationIcao" placeholder="KJFK" maxlength="4" />
|
<input v-model="manualForm.destinationIcao" placeholder="KJFK" maxlength="4"/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -379,31 +396,31 @@
|
|||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>City</span>
|
<span>City</span>
|
||||||
<input v-model="manualForm.departureCity" placeholder="Frankfurt" />
|
<input v-model="manualForm.departureCity" placeholder="Frankfurt"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Airport name</span>
|
<span>Airport name</span>
|
||||||
<input v-model="manualForm.departureName" placeholder="Frankfurt/Main" />
|
<input v-model="manualForm.departureName" placeholder="Frankfurt/Main"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Stand</span>
|
<span>Stand</span>
|
||||||
<input v-model="manualForm.stand" placeholder="A12" />
|
<input v-model="manualForm.stand" placeholder="A12"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Taxi route</span>
|
<span>Taxi route</span>
|
||||||
<input v-model="manualForm.taxiRoute" placeholder="N3 U4" />
|
<input v-model="manualForm.taxiRoute" placeholder="N3 U4"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Runway</span>
|
<span>Runway</span>
|
||||||
<input v-model="manualForm.departureRunway" placeholder="25C" />
|
<input v-model="manualForm.departureRunway" placeholder="25C"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>SID</span>
|
<span>SID</span>
|
||||||
<input v-model="manualForm.sid" placeholder="ANEKI 7S" />
|
<input v-model="manualForm.sid" placeholder="ANEKI 7S"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Transition</span>
|
<span>Transition</span>
|
||||||
<input v-model="manualForm.transition" placeholder="ANEKI" />
|
<input v-model="manualForm.transition" placeholder="ANEKI"/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -431,35 +448,35 @@
|
|||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>City</span>
|
<span>City</span>
|
||||||
<input v-model="manualForm.destinationCity" placeholder="New York" />
|
<input v-model="manualForm.destinationCity" placeholder="New York"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Airport name</span>
|
<span>Airport name</span>
|
||||||
<input v-model="manualForm.destinationName" placeholder="John F. Kennedy" />
|
<input v-model="manualForm.destinationName" placeholder="John F. Kennedy"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Runway</span>
|
<span>Runway</span>
|
||||||
<input v-model="manualForm.arrivalRunway" placeholder="22R" />
|
<input v-model="manualForm.arrivalRunway" placeholder="22R"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>STAR</span>
|
<span>STAR</span>
|
||||||
<input v-model="manualForm.arrivalStar" placeholder="ROBER 3" />
|
<input v-model="manualForm.arrivalStar" placeholder="ROBER 3"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Transition</span>
|
<span>Transition</span>
|
||||||
<input v-model="manualForm.arrivalTransition" placeholder="ROBER" />
|
<input v-model="manualForm.arrivalTransition" placeholder="ROBER"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Approach</span>
|
<span>Approach</span>
|
||||||
<input v-model="manualForm.approach" placeholder="ILS 22R" />
|
<input v-model="manualForm.approach" placeholder="ILS 22R"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Arrival stand</span>
|
<span>Arrival stand</span>
|
||||||
<input v-model="manualForm.arrivalStand" placeholder="Gate 5" />
|
<input v-model="manualForm.arrivalStand" placeholder="Gate 5"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Taxi-in</span>
|
<span>Taxi-in</span>
|
||||||
<input v-model="manualForm.arrivalTaxiRoute" placeholder="B K5" />
|
<input v-model="manualForm.arrivalTaxiRoute" placeholder="B K5"/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -477,7 +494,8 @@
|
|||||||
<v-icon size="20">mdi-altimeter</v-icon>
|
<v-icon size="20">mdi-altimeter</v-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="optional-body">
|
<div class="optional-body">
|
||||||
<div class="manual-card-title">Altitude & codes <span class="optional-chip">optional</span></div>
|
<div class="manual-card-title">Altitude & codes <span class="optional-chip">optional</span>
|
||||||
|
</div>
|
||||||
<p class="muted small">Initial altitudes, squawk, push timing and remarks.</p>
|
<p class="muted small">Initial altitudes, squawk, push timing and remarks.</p>
|
||||||
</div>
|
</div>
|
||||||
<v-icon size="18" class="chevron">mdi-chevron-down</v-icon>
|
<v-icon size="18" class="chevron">mdi-chevron-down</v-icon>
|
||||||
@@ -487,23 +505,23 @@
|
|||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Initial altitude (ft)</span>
|
<span>Initial altitude (ft)</span>
|
||||||
<input v-model="manualForm.initialAltitude" inputmode="numeric" placeholder="5000" />
|
<input v-model="manualForm.initialAltitude" inputmode="numeric" placeholder="5000"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Climb altitude (ft)</span>
|
<span>Climb altitude (ft)</span>
|
||||||
<input v-model="manualForm.climbAltitude" inputmode="numeric" placeholder="7000" />
|
<input v-model="manualForm.climbAltitude" inputmode="numeric" placeholder="7000"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Squawk</span>
|
<span>Squawk</span>
|
||||||
<input v-model="manualForm.squawk" placeholder="4213" />
|
<input v-model="manualForm.squawk" placeholder="4213"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Push delay (min)</span>
|
<span>Push delay (min)</span>
|
||||||
<input v-model="manualForm.pushDelay" inputmode="numeric" placeholder="5" />
|
<input v-model="manualForm.pushDelay" inputmode="numeric" placeholder="5"/>
|
||||||
</label>
|
</label>
|
||||||
<label class="field wide">
|
<label class="field wide">
|
||||||
<span>Briefing notes</span>
|
<span>Briefing notes</span>
|
||||||
<input v-model="manualForm.remarks" placeholder="Optional remarks" />
|
<input v-model="manualForm.remarks" placeholder="Optional remarks"/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -571,12 +589,14 @@
|
|||||||
|
|
||||||
<div v-else class="plan-panel simbrief-panel">
|
<div v-else class="plan-panel simbrief-panel">
|
||||||
<div class="simbrief-hero-card">
|
<div class="simbrief-hero-card">
|
||||||
<NuxtImg src="/img/learn/missions/full-flight/briefing-hero.png" alt="SimBrief import preview" class="simbrief-hero-art" format="webp" />
|
<NuxtImg src="/img/learn/missions/full-flight/briefing-hero.png" alt="SimBrief import preview"
|
||||||
|
class="simbrief-hero-art" format="webp"/>
|
||||||
<div class="simbrief-hero-overlay">
|
<div class="simbrief-hero-overlay">
|
||||||
<span class="simbrief-tag">SimBrief import</span>
|
<span class="simbrief-tag">SimBrief import</span>
|
||||||
<h3 class="simbrief-hero-title">Load your airline dispatch</h3>
|
<h3 class="simbrief-hero-title">Load your airline dispatch</h3>
|
||||||
<p class="simbrief-hero-text">
|
<p class="simbrief-hero-text">
|
||||||
Sync the exact OFP you're flying with one click. We'll transform it into a mission-ready briefing and readback drill.
|
Sync the exact OFP you're flying with one click. We'll transform it into a mission-ready briefing and
|
||||||
|
readback drill.
|
||||||
</p>
|
</p>
|
||||||
<div class="simbrief-hero-highlights">
|
<div class="simbrief-hero-highlights">
|
||||||
<span><v-icon size="16">mdi-airplane-cog</v-icon> Real routes</span>
|
<span><v-icon size="16">mdi-airplane-cog</v-icon> Real routes</span>
|
||||||
@@ -607,7 +627,8 @@
|
|||||||
<span class="step-number">2</span>
|
<span class="step-number">2</span>
|
||||||
<span class="simbrief-step-title">Copy your Pilot ID</span>
|
<span class="simbrief-step-title">Copy your Pilot ID</span>
|
||||||
</div>
|
</div>
|
||||||
<p>Find the ID on the share link of the finished OFP — it's the number we use to fetch your dispatch.</p>
|
<p>Find the ID on the share link of the finished OFP — it's the number we use to fetch your
|
||||||
|
dispatch.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="simbrief-step-card">
|
<div class="simbrief-step-card">
|
||||||
<div class="simbrief-step-head">
|
<div class="simbrief-step-head">
|
||||||
@@ -644,7 +665,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div v-if="simbriefForm.loading" class="simbrief-status">
|
<div v-if="simbriefForm.loading" class="simbrief-status">
|
||||||
<v-progress-circular indeterminate color="cyan" size="20" />
|
<v-progress-circular indeterminate color="cyan" size="20"/>
|
||||||
<span class="muted small">Contacting SimBrief…</span>
|
<span class="muted small">Contacting SimBrief…</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="flightPlanError" class="error-banner">
|
<div v-if="flightPlanError" class="error-banner">
|
||||||
@@ -667,7 +688,7 @@
|
|||||||
|
|
||||||
<div v-else-if="moduleStage==='briefing' && briefingSnapshot" class="module-stage-panel mission-briefing">
|
<div v-else-if="moduleStage==='briefing' && briefingSnapshot" class="module-stage-panel mission-briefing">
|
||||||
<section class="briefing-hero-banner">
|
<section class="briefing-hero-banner">
|
||||||
<NuxtImg :src="currentBriefingArt" alt="Mission hero" class="briefing-hero-bg" />
|
<NuxtImg :src="currentBriefingArt" alt="Mission hero" class="briefing-hero-bg"/>
|
||||||
<div class="briefing-hero-content">
|
<div class="briefing-hero-content">
|
||||||
<div class="briefing-tag-row">
|
<div class="briefing-tag-row">
|
||||||
<span class="plan-tag">Mission briefing</span>
|
<span class="plan-tag">Mission briefing</span>
|
||||||
@@ -714,7 +735,8 @@
|
|||||||
<div class="briefing-layout">
|
<div class="briefing-layout">
|
||||||
<div class="briefing-main-grid">
|
<div class="briefing-main-grid">
|
||||||
<div class="briefing-card">
|
<div class="briefing-card">
|
||||||
<NuxtImg src="/img/learn/missions/full-flight/briefing-route.png" alt="Route preview" class="briefing-card-art" format="webp" />
|
<NuxtImg src="/img/learn/missions/full-flight/briefing-route.png" alt="Route preview"
|
||||||
|
class="briefing-card-art" format="webp"/>
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<v-icon size="16">mdi-map-marker-path</v-icon>
|
<v-icon size="16">mdi-map-marker-path</v-icon>
|
||||||
Flight deck setup
|
Flight deck setup
|
||||||
@@ -722,11 +744,14 @@
|
|||||||
<ul class="briefing-list">
|
<ul class="briefing-list">
|
||||||
<li><strong>Push</strong>: {{ briefingSnapshot.codes.push }}</li>
|
<li><strong>Push</strong>: {{ briefingSnapshot.codes.push }}</li>
|
||||||
<li><strong>ATIS</strong>: Information {{ briefingSnapshot.departure.atis }}</li>
|
<li><strong>ATIS</strong>: Information {{ briefingSnapshot.departure.atis }}</li>
|
||||||
<li><strong>Delivery</strong>: {{ briefingSnapshot.departure.freq }} · {{ briefingSnapshot.departure.freqWords }}</li>
|
<li><strong>Delivery</strong>: {{ briefingSnapshot.departure.freq }} ·
|
||||||
|
{{ briefingSnapshot.departure.freqWords }}
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="briefing-card">
|
<div class="briefing-card">
|
||||||
<NuxtImg src="/img/learn/missions/full-flight/briefing-departure.png" alt="Departure" class="briefing-card-art" format="webp" />
|
<NuxtImg src="/img/learn/missions/full-flight/briefing-departure.png" alt="Departure"
|
||||||
|
class="briefing-card-art" format="webp"/>
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<v-icon size="16">mdi-airplane-takeoff</v-icon>
|
<v-icon size="16">mdi-airplane-takeoff</v-icon>
|
||||||
Departure flow
|
Departure flow
|
||||||
@@ -734,25 +759,32 @@
|
|||||||
<ul class="briefing-list">
|
<ul class="briefing-list">
|
||||||
<li><strong>Stand</strong>: {{ briefingSnapshot.departure.stand || 'As assigned' }}</li>
|
<li><strong>Stand</strong>: {{ briefingSnapshot.departure.stand || 'As assigned' }}</li>
|
||||||
<li><strong>Taxi</strong>: {{ briefingSnapshot.departure.taxiRoute || 'As assigned' }}</li>
|
<li><strong>Taxi</strong>: {{ briefingSnapshot.departure.taxiRoute || 'As assigned' }}</li>
|
||||||
<li><strong>SID</strong>: {{ briefingSnapshot.departure.sid }} · {{ briefingSnapshot.departure.transition }}</li>
|
<li><strong>SID</strong>: {{ briefingSnapshot.departure.sid }} ·
|
||||||
|
{{ briefingSnapshot.departure.transition }}
|
||||||
|
</li>
|
||||||
<li><strong>Initial altitude</strong>: {{ briefingSnapshot.altitudes.initial }}</li>
|
<li><strong>Initial altitude</strong>: {{ briefingSnapshot.altitudes.initial }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="briefing-card">
|
<div class="briefing-card">
|
||||||
<NuxtImg src="/img/learn/missions/full-flight/briefing-arrival.png" alt="Arrival" class="briefing-card-art" format="webp" />
|
<NuxtImg src="/img/learn/missions/full-flight/briefing-arrival.png" alt="Arrival"
|
||||||
|
class="briefing-card-art" format="webp"/>
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<v-icon size="16">mdi-airplane-landing</v-icon>
|
<v-icon size="16">mdi-airplane-landing</v-icon>
|
||||||
Arrival setup
|
Arrival setup
|
||||||
</div>
|
</div>
|
||||||
<ul class="briefing-list">
|
<ul class="briefing-list">
|
||||||
<li><strong>STAR</strong>: {{ briefingSnapshot.arrival.star }} · {{ briefingSnapshot.arrival.transition }}</li>
|
<li><strong>STAR</strong>: {{ briefingSnapshot.arrival.star }} · {{
|
||||||
|
briefingSnapshot.arrival.transition
|
||||||
|
}}
|
||||||
|
</li>
|
||||||
<li><strong>Approach</strong>: {{ briefingSnapshot.arrival.approach }}</li>
|
<li><strong>Approach</strong>: {{ briefingSnapshot.arrival.approach }}</li>
|
||||||
<li><strong>Taxi-in</strong>: {{ briefingSnapshot.arrival.taxiRoute || 'As assigned' }}</li>
|
<li><strong>Taxi-in</strong>: {{ briefingSnapshot.arrival.taxiRoute || 'As assigned' }}</li>
|
||||||
<li><strong>Arrival stand</strong>: {{ briefingSnapshot.arrival.stand || 'As assigned' }}</li>
|
<li><strong>Arrival stand</strong>: {{ briefingSnapshot.arrival.stand || 'As assigned' }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="briefing-card">
|
<div class="briefing-card">
|
||||||
<NuxtImg src="/img/learn/missions/full-flight/briefing-weather.png" alt="Weather" class="briefing-card-art" format="webp" />
|
<NuxtImg src="/img/learn/missions/full-flight/briefing-weather.png" alt="Weather"
|
||||||
|
class="briefing-card-art" format="webp"/>
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<v-icon size="16">mdi-weather-cloudy</v-icon>
|
<v-icon size="16">mdi-weather-cloudy</v-icon>
|
||||||
Weather snapshot
|
Weather snapshot
|
||||||
@@ -776,21 +808,25 @@
|
|||||||
<span class="check-number">1</span>
|
<span class="check-number">1</span>
|
||||||
<div>
|
<div>
|
||||||
<div class="check-title">Clearance & push</div>
|
<div class="check-title">Clearance & push</div>
|
||||||
<p class="muted small">Tune delivery, confirm ATIS {{ briefingSnapshot.departure.atis }} and expect push {{ briefingSnapshot.codes.push }}.</p>
|
<p class="muted small">Tune delivery, confirm ATIS {{ briefingSnapshot.departure.atis }} and expect
|
||||||
|
push {{ briefingSnapshot.codes.push }}.</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="check-number">2</span>
|
<span class="check-number">2</span>
|
||||||
<div>
|
<div>
|
||||||
<div class="check-title">Taxi & departure</div>
|
<div class="check-title">Taxi & departure</div>
|
||||||
<p class="muted small">Follow taxi {{ briefingSnapshot.departure.taxiRoute || 'as assigned' }} to RWY {{ briefingSnapshot.departure.runway }} and fly the {{ briefingSnapshot.departure.sid }}.</p>
|
<p class="muted small">Follow taxi {{ briefingSnapshot.departure.taxiRoute || 'as assigned' }} to
|
||||||
|
RWY {{ briefingSnapshot.departure.runway }} and fly the {{ briefingSnapshot.departure.sid }}.</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="check-number">3</span>
|
<span class="check-number">3</span>
|
||||||
<div>
|
<div>
|
||||||
<div class="check-title">Arrival briefing</div>
|
<div class="check-title">Arrival briefing</div>
|
||||||
<p class="muted small">Plan for {{ briefingSnapshot.arrival.star }} leading to {{ briefingSnapshot.arrival.approach }} and taxi to {{ briefingSnapshot.arrival.stand || 'assigned stand' }}.</p>
|
<p class="muted small">Plan for {{ briefingSnapshot.arrival.star }} leading to
|
||||||
|
{{ briefingSnapshot.arrival.approach }} and taxi to
|
||||||
|
{{ briefingSnapshot.arrival.stand || 'assigned stand' }}.</p>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
@@ -809,8 +845,8 @@
|
|||||||
Adjust plan
|
Adjust plan
|
||||||
</button>
|
</button>
|
||||||
<button class="btn primary" type="button" @click="handleBriefingConfirm()">
|
<button class="btn primary" type="button" @click="handleBriefingConfirm()">
|
||||||
<v-icon size="18">{{ briefingReturnStage==='setup' ? 'mdi-airplane' : 'mdi-play-circle' }}</v-icon>
|
<v-icon size="18">{{ briefingReturnStage === 'setup' ? 'mdi-airplane' : 'mdi-play-circle' }}</v-icon>
|
||||||
{{ briefingReturnStage==='setup' ? 'Start mission' : 'Return to mission' }}
|
{{ briefingReturnStage === 'setup' ? 'Start mission' : 'Return to mission' }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1008,7 +1044,9 @@
|
|||||||
<template v-for="(segment, idx) in activeLesson.readback"
|
<template v-for="(segment, idx) in activeLesson.readback"
|
||||||
:key="segment.type === 'field' ? `f-${segment.key}` : `t-${idx}`">
|
:key="segment.type === 'field' ? `f-${segment.key}` : `t-${idx}`">
|
||||||
<span v-if="segment.type === 'text'" class="cloze-chunk cloze-text">
|
<span v-if="segment.type === 'text'" class="cloze-chunk cloze-text">
|
||||||
{{ displayCallsign(typeof segment.text === 'function' && scenario ? segment.text(scenario) : segment.text) }}
|
{{
|
||||||
|
displayCallsign(typeof segment.text === 'function' && scenario ? segment.text(scenario) : segment.text)
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<label
|
<label
|
||||||
v-else
|
v-else
|
||||||
@@ -1065,7 +1103,8 @@
|
|||||||
<div class="lesson-tip-body">
|
<div class="lesson-tip-body">
|
||||||
<div class="lesson-tip-title">Did you know?</div>
|
<div class="lesson-tip-title">Did you know?</div>
|
||||||
<p class="muted small">
|
<p class="muted small">
|
||||||
Use the dice icon “New scenario” to rehearse the same call with fresh data instantly. Just click it and you'll
|
Use the dice icon “New scenario” to rehearse the same call with fresh data instantly. Just click it and
|
||||||
|
you'll
|
||||||
get different values — repeat that 5–10 times per lesson and the radio call will really stick.
|
get different values — repeat that 5–10 times per lesson and the radio call will really stick.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1075,7 +1114,6 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- NEXT OBJECTIVE -->
|
<!-- NEXT OBJECTIVE -->
|
||||||
<div class="container" v-if="panel==='hub'">
|
<div class="container" v-if="panel==='hub'">
|
||||||
<h2 class="h2">Your progress</h2>
|
<h2 class="h2">Your progress</h2>
|
||||||
@@ -1167,10 +1205,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="container footer-container">
|
<div v-else class="container footer-container">
|
||||||
<div class="footer-meta">
|
<div class="footer-meta gap-2 flex items-center justify-center">
|
||||||
<span class="muted small">© 2025 OpenSquawk. All rights reserved.</span>
|
<span class="muted small">
|
||||||
<NuxtLink to="/feedback" target="_blank" class="link ml-2">Give feedback ›</NuxtLink>
|
<a href="/" class="link">
|
||||||
<a href="/" class="link ml-4">Back to opensquawk.de ›</a>
|
‹ Visit home</a>
|
||||||
|
</span>
|
||||||
|
·
|
||||||
|
<NuxtLink to="/feedback" target="_blank" class="link">Give feedback ›</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
@@ -1278,7 +1319,14 @@ import {useAuthStore} from '~/stores/auth'
|
|||||||
import {createDefaultLearnConfig} from '~~/shared/learn/config'
|
import {createDefaultLearnConfig} from '~~/shared/learn/config'
|
||||||
import type {LearnConfig, LearnProgress, LearnState} from '~~/shared/learn/config'
|
import type {LearnConfig, LearnProgress, LearnState} from '~~/shared/learn/config'
|
||||||
import {learnModules, seedFullFlightScenario} from '~~/shared/data/learnModules'
|
import {learnModules, seedFullFlightScenario} from '~~/shared/data/learnModules'
|
||||||
import {createBaseScenario, digitsToWords, lettersToNato, runwayToWords, altitudeToWords, minutesToWords} from '~~/shared/learn/scenario'
|
import {
|
||||||
|
createBaseScenario,
|
||||||
|
digitsToWords,
|
||||||
|
lettersToNato,
|
||||||
|
runwayToWords,
|
||||||
|
altitudeToWords,
|
||||||
|
minutesToWords
|
||||||
|
} from '~~/shared/learn/scenario'
|
||||||
import type {BlankWidth, Frequency, Lesson, LessonField, ModuleDef, Scenario} from '~~/shared/learn/types'
|
import type {BlankWidth, Frequency, Lesson, LessonField, ModuleDef, Scenario} from '~~/shared/learn/types'
|
||||||
import {loadPizzicatoLite} from '~~/shared/utils/pizzicatoLite'
|
import {loadPizzicatoLite} from '~~/shared/utils/pizzicatoLite'
|
||||||
import type {PizzicatoLite} from '~~/shared/utils/pizzicatoLite'
|
import type {PizzicatoLite} from '~~/shared/utils/pizzicatoLite'
|
||||||
@@ -1396,8 +1444,8 @@ const lessonSearchResults = computed<LessonSearchHit[]>(() => {
|
|||||||
modules.value.forEach(module => {
|
modules.value.forEach(module => {
|
||||||
module.lessons.forEach(lesson => {
|
module.lessons.forEach(lesson => {
|
||||||
const searchable = [lesson.title, lesson.desc, module.title, module.subtitle, (lesson.keywords || []).join(' ')]
|
const searchable = [lesson.title, lesson.desc, module.title, module.subtitle, (lesson.keywords || []).join(' ')]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')
|
.join(' ')
|
||||||
const haystack = norm(searchable)
|
const haystack = norm(searchable)
|
||||||
const matchesTerms = terms.every(term => haystack.includes(term))
|
const matchesTerms = terms.every(term => haystack.includes(term))
|
||||||
const scores = [
|
const scores = [
|
||||||
@@ -1421,15 +1469,15 @@ const lessonSearchResults = computed<LessonSearchHit[]>(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return results
|
return results
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (b.score === a.score) {
|
if (b.score === a.score) {
|
||||||
const moduleCompare = a.module.title.localeCompare(b.module.title)
|
const moduleCompare = a.module.title.localeCompare(b.module.title)
|
||||||
if (moduleCompare !== 0) return moduleCompare
|
if (moduleCompare !== 0) return moduleCompare
|
||||||
return a.lesson.title.localeCompare(b.lesson.title)
|
return a.lesson.title.localeCompare(b.lesson.title)
|
||||||
}
|
}
|
||||||
return b.score - a.score
|
return b.score - a.score
|
||||||
})
|
})
|
||||||
.slice(0, 20)
|
.slice(0, 20)
|
||||||
})
|
})
|
||||||
|
|
||||||
type LessonSearchGroup = {
|
type LessonSearchGroup = {
|
||||||
@@ -1455,16 +1503,16 @@ const lessonSearchGroups = computed<LessonSearchGroup[]>(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(groups.values())
|
return Array.from(groups.values())
|
||||||
.map(group => ({
|
.map(group => ({
|
||||||
...group,
|
...group,
|
||||||
hits: group.hits.sort((a, b) => b.score - a.score)
|
hits: group.hits.sort((a, b) => b.score - a.score)
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
if (b.score === a.score) {
|
if (b.score === a.score) {
|
||||||
return a.module.title.localeCompare(b.module.title)
|
return a.module.title.localeCompare(b.module.title)
|
||||||
}
|
}
|
||||||
return b.score - a.score
|
return b.score - a.score
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function computeLessonSearchOverlayStyle(): Record<string, string> {
|
function computeLessonSearchOverlayStyle(): Record<string, string> {
|
||||||
@@ -1582,7 +1630,7 @@ function displayCallsign(value?: string | null, source?: CallsignContext | null)
|
|||||||
if (!value) return ''
|
if (!value) return ''
|
||||||
const context = source ?? scenario.value
|
const context = source ?? scenario.value
|
||||||
if (!context) return value
|
if (!context) return value
|
||||||
const { radioCall, callsign } = context
|
const {radioCall, callsign} = context
|
||||||
if (radioCall && callsign && value.includes(radioCall)) {
|
if (radioCall && callsign && value.includes(radioCall)) {
|
||||||
return value.split(radioCall).join(callsign)
|
return value.split(radioCall).join(callsign)
|
||||||
}
|
}
|
||||||
@@ -1629,7 +1677,7 @@ const manualForm = reactive<ManualForm>({
|
|||||||
})
|
})
|
||||||
const manualErrors = ref<string[]>([])
|
const manualErrors = ref<string[]>([])
|
||||||
const flightPlanError = ref<string | null>(null)
|
const flightPlanError = ref<string | null>(null)
|
||||||
const simbriefForm = reactive({ userId: '', loading: false })
|
const simbriefForm = reactive({userId: '', loading: false})
|
||||||
const simbriefPlanMeta = ref<{ callsign: string; route: string } | null>(null)
|
const simbriefPlanMeta = ref<{ callsign: string; route: string } | null>(null)
|
||||||
const lessonTrack = ref<HTMLElement | null>(null)
|
const lessonTrack = ref<HTMLElement | null>(null)
|
||||||
const moduleOverviewExpanded = ref(false)
|
const moduleOverviewExpanded = ref(false)
|
||||||
@@ -1689,7 +1737,7 @@ const activeExperience = computed<ExperienceOption>(() => {
|
|||||||
async function handleExperienceSelect(option: ExperienceOption) {
|
async function handleExperienceSelect(option: ExperienceOption) {
|
||||||
experienceMenu.value = false
|
experienceMenu.value = false
|
||||||
if (option.matches(route.path)) return
|
if (option.matches(route.path)) return
|
||||||
if( option.target === '_blank') {
|
if (option.target === '_blank') {
|
||||||
window.open(option.to, '_blank')
|
window.open(option.to, '_blank')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1735,10 +1783,10 @@ function computeQuerySignature(query: Record<string, unknown>): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isValidStage = (value: string | null): value is 'lessons' | 'setup' | 'briefing' =>
|
const isValidStage = (value: string | null): value is 'lessons' | 'setup' | 'briefing' =>
|
||||||
value === 'lessons' || value === 'setup' || value === 'briefing'
|
value === 'lessons' || value === 'setup' || value === 'briefing'
|
||||||
|
|
||||||
const isValidPlanMode = (value: string | null): value is FlightPlanMode =>
|
const isValidPlanMode = (value: string | null): value is FlightPlanMode =>
|
||||||
value === 'random' || value === 'manual' || value === 'simbrief'
|
value === 'random' || value === 'manual' || value === 'simbrief'
|
||||||
|
|
||||||
function buildStateRouteQuery(): Record<string, string> {
|
function buildStateRouteQuery(): Record<string, string> {
|
||||||
const state: Record<string, string> = {}
|
const state: Record<string, string> = {}
|
||||||
@@ -1896,25 +1944,25 @@ function applyRouteStateFromQuery(query: RouteQueryLike) {
|
|||||||
|
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
watch(
|
watch(
|
||||||
() => [panel.value, current.value?.id ?? null, moduleStage.value, activeLesson.value?.id ?? null, flightPlanMode.value],
|
() => [panel.value, current.value?.id ?? null, moduleStage.value, activeLesson.value?.id ?? null, flightPlanMode.value],
|
||||||
() => {
|
() => {
|
||||||
void syncRouteFromState()
|
void syncRouteFromState()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query,
|
() => route.query,
|
||||||
newQuery => {
|
newQuery => {
|
||||||
if (isSyncingRoute) {
|
if (isSyncingRoute) {
|
||||||
lastSyncedQuerySignature = computeQuerySignature(newQuery as Record<string, unknown>)
|
lastSyncedQuerySignature = computeQuerySignature(newQuery as Record<string, unknown>)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
const signature = computeQuerySignature(newQuery as Record<string, unknown>)
|
||||||
|
if (signature === lastSyncedQuerySignature) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
applyRouteStateFromQuery(newQuery as RouteQueryLike)
|
||||||
}
|
}
|
||||||
const signature = computeQuerySignature(newQuery as Record<string, unknown>)
|
|
||||||
if (signature === lastSyncedQuerySignature) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
applyRouteStateFromQuery(newQuery as RouteQueryLike)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2002,16 +2050,16 @@ const flightPlanModes: Array<{ id: FlightPlanMode; title: string; icon: string;
|
|||||||
]
|
]
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => simbriefForm.userId,
|
() => simbriefForm.userId,
|
||||||
value => {
|
value => {
|
||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
const trimmed = value.trim()
|
const trimmed = value.trim()
|
||||||
if (trimmed) {
|
if (trimmed) {
|
||||||
window.localStorage.setItem(SIMBRIEF_STORAGE_KEY, trimmed)
|
window.localStorage.setItem(SIMBRIEF_STORAGE_KEY, trimmed)
|
||||||
} else {
|
} else {
|
||||||
window.localStorage.removeItem(SIMBRIEF_STORAGE_KEY)
|
window.localStorage.removeItem(SIMBRIEF_STORAGE_KEY)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
function toggleManualSection(section: ManualSection) {
|
function toggleManualSection(section: ManualSection) {
|
||||||
@@ -2410,7 +2458,7 @@ async function loadSimbriefPlan() {
|
|||||||
manualErrors.value = []
|
manualErrors.value = []
|
||||||
simbriefForm.loading = true
|
simbriefForm.loading = true
|
||||||
try {
|
try {
|
||||||
const response = await api.get<{ data: any }>('/api/learn/simbrief', { query: { userId }, auth: true })
|
const response = await api.get<{ data: any }>('/api/learn/simbrief', {query: {userId}, auth: true})
|
||||||
const payload = response?.data
|
const payload = response?.data
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
flightPlanError.value = 'No SimBrief dispatch found.'
|
flightPlanError.value = 'No SimBrief dispatch found.'
|
||||||
@@ -2614,6 +2662,7 @@ function normalizeSimbriefPlan(raw: any): MissionPlanInput {
|
|||||||
|
|
||||||
return plan
|
return plan
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasSpokenTarget = ref(false)
|
const hasSpokenTarget = ref(false)
|
||||||
const pendingAutoSay = ref(false)
|
const pendingAutoSay = ref(false)
|
||||||
const activeFrequency = ref<Frequency | null>(null)
|
const activeFrequency = ref<Frequency | null>(null)
|
||||||
@@ -2848,9 +2897,9 @@ type LearnStateResponse = LearnState
|
|||||||
function sanitizeModuleList(value: unknown): string[] {
|
function sanitizeModuleList(value: unknown): string[] {
|
||||||
if (!Array.isArray(value)) return []
|
if (!Array.isArray(value)) return []
|
||||||
const sanitized = value
|
const sanitized = value
|
||||||
.filter(item => typeof item === 'string')
|
.filter(item => typeof item === 'string')
|
||||||
.map(item => item.trim())
|
.map(item => item.trim())
|
||||||
.filter(item => item.length > 0)
|
.filter(item => item.length > 0)
|
||||||
return Array.from(new Set(sanitized))
|
return Array.from(new Set(sanitized))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3313,8 +3362,8 @@ const missionFooterPrimary = computed(() => {
|
|||||||
const lessonAnswerSignature = computed(() => {
|
const lessonAnswerSignature = computed(() => {
|
||||||
if (!activeLesson.value) return ''
|
if (!activeLesson.value) return ''
|
||||||
return activeLesson.value.fields
|
return activeLesson.value.fields
|
||||||
.map(field => (userAnswers[field.key] ?? '').trim())
|
.map(field => (userAnswers[field.key] ?? '').trim())
|
||||||
.join('|')
|
.join('|')
|
||||||
})
|
})
|
||||||
|
|
||||||
const previousActionLabel = computed(() => {
|
const previousActionLabel = computed(() => {
|
||||||
@@ -5569,9 +5618,8 @@ onMounted(() => {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
border-color: color-mix(in srgb, var(--accent) 38%, transparent);
|
border-color: color-mix(in srgb, var(--accent) 38%, transparent);
|
||||||
background:
|
background: radial-gradient(420px 260px at -10% -20%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%),
|
||||||
radial-gradient(420px 260px at -10% -20%, color-mix(in srgb, var(--accent) 22%, transparent), transparent 70%),
|
linear-gradient(150deg, color-mix(in srgb, var(--bg2) 82%, transparent), color-mix(in srgb, var(--text) 6%, transparent));
|
||||||
linear-gradient(150deg, color-mix(in srgb, var(--bg2) 82%, transparent), color-mix(in srgb, var(--text) 6%, transparent));
|
|
||||||
--module-overview-gap: 28px;
|
--module-overview-gap: 28px;
|
||||||
--lesson-track-max-height: 400px;
|
--lesson-track-max-height: 400px;
|
||||||
--lesson-track-opacity: 1;
|
--lesson-track-opacity: 1;
|
||||||
@@ -7645,33 +7693,42 @@ onMounted(() => {
|
|||||||
.plan-mode-card {
|
.plan-mode-card {
|
||||||
min-width: 180px;
|
min-width: 180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-summary {
|
.plan-summary {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-hero {
|
.plan-hero {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 140px;
|
height: 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-actions .btn,
|
.plan-actions .btn,
|
||||||
.briefing-actions .btn {
|
.briefing-actions .btn {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.manual-grid {
|
.manual-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.manual-preview {
|
.manual-preview {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
|
|
||||||
.briefing-hero-content {
|
.briefing-hero-content {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.briefing-hero-title {
|
.briefing-hero-title {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.briefing-layout {
|
.briefing-layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.briefing-sidebar {
|
.briefing-sidebar {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
@@ -7697,10 +7754,12 @@ onMounted(() => {
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hud-right[data-v-06cbe329] {
|
.hud-right[data-v-06cbe329] {
|
||||||
/* justify-content: flex-start; */
|
/* justify-content: flex-start; */
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hud-inner {
|
.hud-inner {
|
||||||
.sep, .hud-divider, .brand {
|
.sep, .hud-divider, .brand {
|
||||||
@apply hidden
|
@apply hidden
|
||||||
|
|||||||
Reference in New Issue
Block a user