Training Mission Hub

-
Start with the ICAO alphabet & numbers, then basics, ground, and more.
+
Master radio communications from basics to emergencies.
-
-
-
- - mdi-star - New briefing - -
-
-
-
- mdi-flag-checkered - {{ m.title }} -
- -
-
{{ m.subtitle }}
+
+
+

{{ track.title }}

+
{{ track.subtitle }}
+
-
-
-
- {{ doneCount(m.id) }}/{{ m.lessons.length }} lessons - {{ moduleHasProgress(m.id) ? avgScore(m.id) + '%' : '—' }} avg -
-
- - +
-
-
- mdi-lock-alert -
-
Clearance pending
-
- Complete earlier missions to - - this briefing. +
+ +
+
+
+ + mdi-star + New briefing + +
+
+
+
+ mdi-flag-checkered + {{ m.title }} +
+
+
{{ m.subtitle }}
+
+
+
+
+ {{ doneCount(m.id) }}/{{ m.lessons.length }} lessons + {{ moduleHasProgress(m.id) ? avgScore(m.id) + '%' : '—' }} avg +
+
+ +
+
+
+
+ mdi-lock-alert +
+
Clearance pending
+
+ Complete earlier missions to + + this briefing. +
@@ -1318,7 +1328,7 @@ import {useApi} from '~/composables/useApi' import {useAuthStore} from '~/stores/auth' import {createDefaultLearnConfig} from '~~/shared/learn/config' import type {LearnConfig, LearnProgress, LearnState} from '~~/shared/learn/config' -import {learnModules, seedFullFlightScenario} from '~~/shared/data/learnModules' +import {learnModules, learnTracks, seedFullFlightScenario} from '~~/shared/data/learnModules' import { createBaseScenario, digitsToWords, @@ -1327,7 +1337,7 @@ import { 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, TrackDef} from '~~/shared/learn/types' import {loadPizzicatoLite} from '~~/shared/utils/pizzicatoLite' import type {PizzicatoLite} from '~~/shared/utils/pizzicatoLite' import {createNoiseGenerators, getReadabilityProfile} from '~~/shared/utils/radioEffects' @@ -1422,7 +1432,8 @@ function tightenedThreshold(length: number, base: number): number { return Math.max(base, 0.97) } -const modules = shallowRef(learnModules) +const tracks = shallowRef(learnTracks) +const modules = computed(() => tracks.value.flatMap(t => t.modules)) type LessonSearchHit = { module: ModuleDef @@ -3766,6 +3777,15 @@ function pct(modId: string) { return Math.round((doneCount(modId) / module.lessons.length) * 100) } +function trackPct(trackId: string): number { + const track = tracks.value.find(t => t.id === trackId) + if (!track) return 0 + const total = track.modules.reduce((sum, m) => sum + m.lessons.length, 0) + if (total === 0) return 0 + const done = track.modules.reduce((sum, m) => sum + doneCount(m.id), 0) + return Math.round((done / total) * 100) +} + function moduleNumber(modId: string): number { const index = modules.value.findIndex(module => module.id === modId) return index >= 0 ? index + 1 : 0 @@ -5330,6 +5350,35 @@ onMounted(() => { margin: 6px 0 10px } +.track-section { + margin-bottom: 2.5rem; +} + +.track-header { + margin-bottom: 1rem; +} + +.track-title { + font-size: 1.15rem; + font-weight: 600; + letter-spacing: 0.02em; + color: var(--text-primary, #e0e0e0); +} + +.track-subtitle { + font-size: 0.85rem; + margin-top: 0.15rem; +} + +.track-progress { + margin-top: 0.5rem; + max-width: 300px; +} + +.track-progress .line { + height: 3px; +} + .tiles { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));