diff --git a/app/middleware/require-classroom-intro.ts b/app/middleware/require-classroom-intro.ts new file mode 100644 index 0000000..e0eff44 --- /dev/null +++ b/app/middleware/require-classroom-intro.ts @@ -0,0 +1,10 @@ +import {CLASSROOM_INTRO_STORAGE_KEY} from '~~/shared/constants/storage' + +export default defineNuxtRouteMiddleware(() => { + if (process.server) return + + const introComplete = localStorage.getItem(CLASSROOM_INTRO_STORAGE_KEY) === 'true' + if (!introComplete) { + return navigateTo('/classroom-introduction') + } +}) diff --git a/app/pages/classroom-introduction.vue b/app/pages/classroom-introduction.vue index 423d838..6643a4e 100644 --- a/app/pages/classroom-introduction.vue +++ b/app/pages/classroom-introduction.vue @@ -13,7 +13,7 @@

Guided preflight briefing

- + Enter Classroom hub @@ -167,7 +167,7 @@ Start guided tour - + Skip to Classroom @@ -382,10 +382,10 @@ Next stop - + Enter Classroom hub - + @@ -403,6 +403,7 @@ import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue' import { useHead } from '#imports' import { useApi } from '~/composables/useApi' +import { CLASSROOM_INTRO_STORAGE_KEY } from '~~/shared/constants/storage' import type { PizzicatoLite } from '~~/shared/utils/pizzicatoLite' import { loadPizzicatoLite } from '~~/shared/utils/pizzicatoLite' import { clampReadability, createNoiseGenerators, getReadabilityProfile } from '~~/shared/utils/radioEffects' @@ -420,6 +421,8 @@ interface StageStop { voiceLine: string } +definePageMeta({ middleware: 'require-auth' }) + useHead({ title: 'Classroom orientation • OpenSquawk' }) const stages: StageStop[] = [ @@ -497,6 +500,15 @@ const stages: StageStop[] = [ const api = useApi() +function markClassroomIntroComplete() { + if (typeof window === 'undefined') return + localStorage.setItem(CLASSROOM_INTRO_STORAGE_KEY, 'true') +} + +function handleClassroomEntry() { + markClassroomIntroComplete() +} + const voiceMode = ref('text') const radioLevel = ref(3) const hasCompletedRadioCheck = ref(false) diff --git a/app/pages/classroom.vue b/app/pages/classroom.vue index 82d89bd..20956b9 100644 --- a/app/pages/classroom.vue +++ b/app/pages/classroom.vue @@ -122,6 +122,10 @@
+ + mdi-message-draw + Feedback +
diff --git a/app/pages/login.vue b/app/pages/login.vue index c8a372a..dfdb3ec 100644 --- a/app/pages/login.vue +++ b/app/pages/login.vue @@ -288,6 +288,7 @@ import {useRoute, useRouter} from 'vue-router' import {useHead} from '#imports' import {useAuthStore} from '~/stores/auth' import {useApi} from '~/composables/useApi' +import {CLASSROOM_INTRO_STORAGE_KEY} from '~~/shared/constants/storage' type Mode = 'login' | 'register' @@ -355,7 +356,7 @@ async function submitLogin() { try { await auth.login({...loginForm}) await auth.fetchUser() - const target = redirectTarget.value || '/classroom' + const target = resolvePostAuthTarget(redirectTarget.value || null) await router.replace(target) } catch (err: any) { const message = err?.data?.statusMessage || err?.message || 'Login failed' @@ -367,6 +368,28 @@ async function submitLogin() { const img = '/img/login/img' + (Math.ceil(Math.random() * 3)) + '.jpeg' +function hasCompletedClassroomIntroduction(): boolean { + if (typeof window === 'undefined') return false + return localStorage.getItem(CLASSROOM_INTRO_STORAGE_KEY) === 'true' +} + +function setClassroomIntroductionComplete(completed: boolean) { + if (typeof window === 'undefined') return + localStorage.setItem(CLASSROOM_INTRO_STORAGE_KEY, completed ? 'true' : 'false') +} + +function resolvePostAuthTarget(preferred?: string | null) { + const introCompleted = hasCompletedClassroomIntroduction() + const fallback = introCompleted ? '/classroom' : '/classroom-introduction' + let target = preferred || fallback + + if (!introCompleted && target.startsWith('/classroom') && target !== '/classroom-introduction') { + target = '/classroom-introduction' + } + + return target +} + async function checkInvitationCode() { if (!registerForm.invitationCode) { invitationStatus.value = 'unknown' @@ -408,8 +431,8 @@ async function submitRegister() { acceptPrivacy: registerForm.acceptPrivacy, }) await auth.fetchUser() - const target = redirectTarget.value || '/classroom' - await router.replace(target) + setClassroomIntroductionComplete(false) + await router.replace('/classroom-introduction') } catch (err: any) { const message = err?.data?.statusMessage || err?.message || 'Registration failed' registerError.value = message @@ -427,7 +450,7 @@ useHead({ onMounted(() => { if (auth.isAuthenticated) { - const target = redirectTarget.value || '/classroom' + const target = resolvePostAuthTarget(redirectTarget.value || null) router.replace(target) } }) diff --git a/shared/constants/storage.ts b/shared/constants/storage.ts new file mode 100644 index 0000000..19ceba5 --- /dev/null +++ b/shared/constants/storage.ts @@ -0,0 +1 @@ +export const CLASSROOM_INTRO_STORAGE_KEY = 'os_classroom_intro_completed'