1- import { useEffect , useMemo , useState } from "react" ;
1+ import { useEffect , useMemo , useRef , useState } from "react" ;
2+ import { ConfirmDialog } from "./components/ConfirmDialog" ;
23import { HomeScreen } from "./components/HomeScreen" ;
34import { LevelOverview } from "./components/LevelOverview" ;
45import { LevelSummaryScreen } from "./components/LevelSummaryScreen" ;
@@ -175,6 +176,10 @@ function getRecommendationLabel(
175176 return "Stay on same level" as const ;
176177}
177178
179+ function isActiveTrainingState ( appState : AppState ) : boolean {
180+ return [ "memorizing" , "pause" , "reconstructing" , "problem-result" ] . includes ( appState ) ;
181+ }
182+
178183export default function App ( ) {
179184 const initialProgress = useMemo ( ( ) => loadProgress ( ) , [ ] ) ;
180185 const initialRoute = useMemo ( ( ) => parseRoute ( ) , [ ] ) ;
@@ -198,6 +203,9 @@ export default function App() {
198203 const [ pauseCountdown , setPauseCountdown ] = useState ( 3 ) ;
199204 const [ lastResultIndex , setLastResultIndex ] = useState < number | null > ( null ) ;
200205 const [ summarySaved , setSummarySaved ] = useState ( false ) ;
206+ const [ pendingExitRoute , setPendingExitRoute ] = useState < Route | null > ( null ) ;
207+ const sessionRef = useRef < TrainingSession | null > ( null ) ;
208+ const appStateRef = useRef < AppState > ( appState ) ;
201209
202210 useEffect ( ( ) => {
203211 const failures = runScoringSelfCheck ( ) ;
@@ -210,6 +218,14 @@ export default function App() {
210218 saveProgress ( progress ) ;
211219 } , [ progress ] ) ;
212220
221+ useEffect ( ( ) => {
222+ sessionRef . current = session ;
223+ } , [ session ] ) ;
224+
225+ useEffect ( ( ) => {
226+ appStateRef . current = appState ;
227+ } , [ appState ] ) ;
228+
213229 useEffect ( ( ) => {
214230 if ( ! window . location . hash ) {
215231 replaceRoute ( { screen : "home" } ) ;
@@ -223,6 +239,18 @@ export default function App() {
223239 useEffect ( ( ) => {
224240 function applyRoute ( ) : void {
225241 const route = parseRoute ( ) ;
242+ const activeSession = sessionRef . current ;
243+ const leavingActiveTraining =
244+ activeSession !== null &&
245+ isActiveTrainingState ( appStateRef . current ) &&
246+ ( route . screen !== "training" || route . level !== activeSession . level ) ;
247+
248+ if ( leavingActiveTraining ) {
249+ replaceRoute ( { screen : "training" , level : activeSession . level } ) ;
250+ setPendingExitRoute ( route ) ;
251+ return ;
252+ }
253+
226254 if ( route . screen === "home" ) {
227255 setSession ( null ) ;
228256 setAppState ( "home" ) ;
@@ -517,11 +545,45 @@ export default function App() {
517545 }
518546
519547 function backHome ( ) : void {
548+ if ( session && isActiveTrainingState ( appState ) ) {
549+ setPendingExitRoute ( { screen : "home" } ) ;
550+ return ;
551+ }
520552 setSession ( null ) ;
521553 setAppState ( "home" ) ;
522554 pushRoute ( { screen : "home" } ) ;
523555 }
524556
557+ function cancelTrainingExit ( ) : void {
558+ setPendingExitRoute ( null ) ;
559+ if ( session && isActiveTrainingState ( appState ) ) {
560+ replaceRoute ( { screen : "training" , level : session . level } ) ;
561+ }
562+ }
563+
564+ function confirmTrainingExit ( ) : void {
565+ if ( ! pendingExitRoute ) {
566+ return ;
567+ }
568+ const nextRoute = pendingExitRoute ;
569+ setPendingExitRoute ( null ) ;
570+ setSession ( null ) ;
571+
572+ if ( nextRoute . screen === "home" ) {
573+ setAppState ( "home" ) ;
574+ pushRoute ( { screen : "home" } ) ;
575+ return ;
576+ }
577+
578+ setSelectedLevel ( nextRoute . level ) ;
579+ setProgress ( ( current ) => ( {
580+ ...current ,
581+ lastSelectedLevel : nextRoute . level ,
582+ } ) ) ;
583+ setAppState ( "level-overview" ) ;
584+ pushRoute ( { screen : "level-overview" , level : nextRoute . level } ) ;
585+ }
586+
525587 const lastSession = progress . sessions [ progress . sessions . length - 1 ] ;
526588 const showWorldChampionshipMessage =
527589 session ?. level === ( LEVELS [ LEVELS . length - 1 ] ?. level ?? null ) &&
@@ -617,6 +679,15 @@ export default function App() {
617679 onBackHome = { backHome }
618680 />
619681 ) : null }
682+ < ConfirmDialog
683+ open = { pendingExitRoute !== null }
684+ title = "Leave training?"
685+ message = "Your current level progress will be lost."
686+ cancelLabel = "Stay in training"
687+ confirmLabel = "Leave training"
688+ onCancel = { cancelTrainingExit }
689+ onConfirm = { confirmTrainingExit }
690+ />
620691 </ main >
621692 ) ;
622693}
0 commit comments