From f039eb0c0a1342cf9fbeb87e8846d01bf2b3c2ed Mon Sep 17 00:00:00 2001 From: itsrubberduck Date: Thu, 9 Jul 2026 16:24:00 +0200 Subject: [PATCH] feat(onboarding): add shared pilot-profile option config --- shared/onboarding/config.test.ts | 50 ++++++++++++ shared/onboarding/config.ts | 136 +++++++++++++++++++++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 shared/onboarding/config.test.ts create mode 100644 shared/onboarding/config.ts diff --git a/shared/onboarding/config.test.ts b/shared/onboarding/config.test.ts new file mode 100644 index 0000000..4ca05cc --- /dev/null +++ b/shared/onboarding/config.test.ts @@ -0,0 +1,50 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { + SIMULATOR_OPTIONS, + OS_OPTIONS, + HARDWARE_OPTIONS, + RADIO_PAIN_POINT_OPTIONS, + TOOLKIT_OPTIONS, + PAID_TOOLKIT_VALUES, + TOOLKIT_DURATION_OPTIONS, + FEATURE_WISH_OPTIONS, + PRICING_PREFERENCE_OPTIONS, + ONBOARDING_TOTAL_STEPS, + createDefaultOnboardingProfile, +} from '~~/shared/onboarding/config' + +describe('onboarding config', () => { + it('every option list has unique, non-empty values', () => { + for (const list of [ + SIMULATOR_OPTIONS, OS_OPTIONS, HARDWARE_OPTIONS, RADIO_PAIN_POINT_OPTIONS, + TOOLKIT_OPTIONS, TOOLKIT_DURATION_OPTIONS, FEATURE_WISH_OPTIONS, PRICING_PREFERENCE_OPTIONS, + ]) { + const values = list.map(o => o.value) + assert.equal(new Set(values).size, values.length, 'duplicate option value found') + assert.ok(values.every(v => typeof v === 'string' && v.length > 0)) + } + }) + + it('PAID_TOOLKIT_VALUES is a subset of TOOLKIT_OPTIONS values', () => { + const toolkitValues = new Set(TOOLKIT_OPTIONS.map(o => o.value)) + for (const paid of PAID_TOOLKIT_VALUES) { + assert.ok(toolkitValues.has(paid), `${paid} must be a valid toolkit option`) + } + }) + + it('ONBOARDING_TOTAL_STEPS matches the number of required screens', () => { + assert.equal(ONBOARDING_TOTAL_STEPS, 7) + }) + + it('createDefaultOnboardingProfile returns empty/undefined fields and no completion timestamps', () => { + const profile = createDefaultOnboardingProfile() + assert.equal(profile.completedAt, null) + assert.equal(profile.skippedAt, null) + assert.deepEqual(profile.hardware, []) + assert.deepEqual(profile.networkExperience, []) + assert.deepEqual(profile.toolkit, []) + assert.deepEqual(profile.topFeatures, []) + assert.deepEqual(profile.toolkitDuration, {}) + }) +}) diff --git a/shared/onboarding/config.ts b/shared/onboarding/config.ts new file mode 100644 index 0000000..ac4901b --- /dev/null +++ b/shared/onboarding/config.ts @@ -0,0 +1,136 @@ +export type Simulator = 'msfs2024' | 'msfs2020' | 'xplane12' | 'other' +export type OperatingSystem = 'windows' | 'mac' | 'linux' +export type HardwareItem = 'keyboard_mouse' | 'hotas' | 'yoke_pedals' | 'multi_monitor' +export type NetworkExperience = 'vatsim' | 'ivao' +export type RadioPainPoint = 'readbacks' | 'fast_controllers' | 'phraseology' | 'accents' | 'none' +export type ToolkitItem = 'navigraph' | 'simbrief' | 'sayintentions' | 'beyondatc' | 'pilotedge' +export type ToolkitDuration = 'trying' | 'few_months' | 'six_plus_months' | 'cant_fly_without' +export type FeatureWish = + | 'live_ai_atc' + | 'more_airports' + | 'network_integration' + | 'structured_lessons' + | 'voice_packs' + | 'faster_stt' + | 'progress_badges' +export type PricingPreference = 'one_time' | 'monthly' | 'season_pass' | 'free_self_host' + +export interface OnboardingOption { + value: T + label: string + icon: string +} + +export const SIMULATOR_OPTIONS: OnboardingOption[] = [ + { value: 'msfs2024', label: 'MSFS 2024', icon: 'mdi-microsoft' }, + { value: 'msfs2020', label: 'MSFS 2020', icon: 'mdi-microsoft' }, + { value: 'xplane12', label: 'X-Plane 12', icon: 'mdi-airplane' }, + { value: 'other', label: 'Something else', icon: 'mdi-help-circle-outline' }, +] + +export const OS_OPTIONS: OnboardingOption[] = [ + { value: 'windows', label: 'Windows', icon: 'mdi-microsoft-windows' }, + { value: 'mac', label: 'macOS', icon: 'mdi-apple' }, + { value: 'linux', label: 'Linux', icon: 'mdi-linux' }, +] + +export const HARDWARE_OPTIONS: OnboardingOption[] = [ + { value: 'keyboard_mouse', label: 'Keyboard & mouse', icon: 'mdi-keyboard-outline' }, + { value: 'hotas', label: 'HOTAS / joystick', icon: 'mdi-controller-classic-outline' }, + { value: 'yoke_pedals', label: 'Yoke & pedals', icon: 'mdi-steering' }, + { value: 'multi_monitor', label: 'Multi-monitor / cockpit build', icon: 'mdi-monitor-multiple' }, +] + +export const RADIO_CONFIDENCE_LABELS: Record = { + 1: 'First solo jitters', + 2: 'Getting the hang of it', + 3: 'Comfortable most days', + 4: 'Rarely rattled', + 5: 'Sounds like a real dispatcher', +} + +export const RADIO_PAIN_POINT_OPTIONS: OnboardingOption[] = [ + { value: 'readbacks', label: 'Getting readbacks right', icon: 'mdi-repeat' }, + { value: 'fast_controllers', label: 'Fast-talking controllers', icon: 'mdi-speedometer' }, + { value: 'phraseology', label: 'Remembering phraseology', icon: 'mdi-book-alphabet' }, + { value: 'accents', label: 'Understanding accents', icon: 'mdi-ear-hearing' }, + { value: 'none', label: "Honestly, I'm solid", icon: 'mdi-check-decagram' }, +] + +export const NETWORK_OPTIONS: OnboardingOption[] = [ + { value: 'vatsim', label: 'VATSIM', icon: 'mdi-account-group' }, + { value: 'ivao', label: 'IVAO', icon: 'mdi-account-group-outline' }, +] + +export const TOOLKIT_OPTIONS: OnboardingOption[] = [ + { value: 'navigraph', label: 'Navigraph', icon: 'mdi-map-marker-path' }, + { value: 'simbrief', label: 'SimBrief', icon: 'mdi-clipboard-flow-outline' }, + { value: 'sayintentions', label: 'SayIntentions', icon: 'mdi-chat-processing-outline' }, + { value: 'beyondatc', label: 'BeyondATC', icon: 'mdi-radio-tower' }, + { value: 'pilotedge', label: 'PilotEdge', icon: 'mdi-account-voice' }, +] + +/** Subscription/paid tools — selecting one of these reveals the "how long?" follow-up. */ +export const PAID_TOOLKIT_VALUES: ToolkitItem[] = ['navigraph', 'sayintentions', 'beyondatc', 'pilotedge'] + +export const TOOLKIT_DURATION_OPTIONS: OnboardingOption[] = [ + { value: 'trying', label: 'Just trying it', icon: 'mdi-flask-outline' }, + { value: 'few_months', label: 'A few months', icon: 'mdi-calendar-month-outline' }, + { value: 'six_plus_months', label: '6+ months', icon: 'mdi-calendar-check-outline' }, + { value: 'cant_fly_without', label: "Can't fly without it", icon: 'mdi-heart' }, +] + +export const FEATURE_WISH_OPTIONS: OnboardingOption[] = [ + { value: 'live_ai_atc', label: 'More realistic live AI ATC', icon: 'mdi-robot-outline' }, + { value: 'more_airports', label: 'More airports & scenarios', icon: 'mdi-airport' }, + { value: 'network_integration', label: 'VATSIM/IVAO-style network integration', icon: 'mdi-lan-connect' }, + { value: 'structured_lessons', label: 'Structured Classroom lessons', icon: 'mdi-school-outline' }, + { value: 'voice_packs', label: 'Custom controller voice packs', icon: 'mdi-account-voice' }, + { value: 'faster_stt', label: 'Faster, offline-capable speech recognition', icon: 'mdi-lightning-bolt-outline' }, + { value: 'progress_badges', label: 'Progress tracking & badges', icon: 'mdi-medal-outline' }, +] + +export const MAX_FEATURE_WISHES = 2 + +export const PRICING_PREFERENCE_OPTIONS: OnboardingOption[] = [ + { value: 'one_time', label: 'One-time unlock — I own it', icon: 'mdi-lock-open-variant-outline' }, + { value: 'monthly', label: 'Small monthly add-on, always latest', icon: 'mdi-calendar-sync-outline' }, + { value: 'season_pass', label: 'Bundle / season pass', icon: 'mdi-ticket-confirmation-outline' }, + { value: 'free_self_host', label: "I'd rather stay free-tier / self-host", icon: 'mdi-server-network-outline' }, +] + +export const ONBOARDING_TOTAL_STEPS = 7 + +export interface OnboardingProfileFields { + simulator: Simulator | null + os: OperatingSystem | null + hardware: HardwareItem[] + radioConfidence: number | null + radioPainPoint: RadioPainPoint | null + networkExperience: NetworkExperience[] + toolkit: ToolkitItem[] + toolkitDuration: Partial> + topFeatures: FeatureWish[] + pricingPreference: PricingPreference | null + resultCallsign: string | null + completedAt: string | null + skippedAt: string | null +} + +export function createDefaultOnboardingProfile(): OnboardingProfileFields { + return { + simulator: null, + os: null, + hardware: [], + radioConfidence: null, + radioPainPoint: null, + networkExperience: [], + toolkit: [], + toolkitDuration: {}, + topFeatures: [], + pricingPreference: null, + resultCallsign: null, + completedAt: null, + skippedAt: null, + } +}