Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 11 additions & 72 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useRegisterSW } from "virtual:pwa-register/react";
import { estimateCalories, getSavedUserWeight } from "./utils/calorieEstimator";
import { CursorGlow } from "./components/CursorGlow";
import { PageErrorBoundary } from "./components/PageErrorBoundary";
import { ExitConfirmModal } from "./components/ExitConfirmModal";
const WelcomeScreen = lazy(() => import("./components/WelcomeScreen").then(m => ({ default: m.WelcomeScreen })));
const SummaryScreen = lazy(() => import("./components/SummaryScreen").then(m => ({ default: m.SummaryScreen })));
const TrophyRoom = lazy(() => import("./components/TrophyRoom").then(m => ({ default: m.TrophyRoom })));
Expand Down Expand Up @@ -488,79 +489,17 @@ function App() {
</div>
)}
{showExitModal && (
<div
style={{
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100%",
background: "rgba(0,0,0,0.5)",
display: "flex",
justifyContent: "center",
alignItems: "center",
zIndex: 999,
backdropFilter: "blur(8px)",
<ExitConfirmModal
message="Are you sure you want to end your session?"
onStay={() => setShowExitModal(false)}
onExit={() => {
setShowExitModal(false);
if (user?.uid) {
localStorage.removeItem(`spectrax_telemetry_snapshot_${user.uid}`);
}
navigateTo('welcome');
}}
>
<div
style={{
background: "rgba(255,255,255,0.1)",
border: "1px solid rgba(255,255,255,0.2)",
borderRadius: "20px",
padding: "30px",
width: "320px",
textAlign: "center",
color: "white",
backdropFilter: "blur(15px)",
boxShadow: "0 8px 32px rgba(0,0,0,0.3)",
}}
>
<h2>Confirm Exit</h2>

<p>Are you sure you want to end your session?</p>

<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: "20px",
}}
>
<button
onClick={() => setShowExitModal(false)}
style={{
padding: "10px 20px",
borderRadius: "10px",
border: "none",
cursor: "pointer",
}}
>
Stay
</button>

<button
onClick={() => {
setShowExitModal(false);
if (user?.uid) {
localStorage.removeItem(`spectrax_telemetry_snapshot_${user.uid}`);
}
navigateTo('welcome');
}}
style={{
padding: '10px 20px',
borderRadius: '10px',
border: 'none',
cursor: 'pointer',
background: '#ff4d4f',
color: 'white'
}}
>
Exit
</button>
</div>
</div>
</div>
/>
)}
</main>
);
Expand Down
84 changes: 84 additions & 0 deletions src/components/ExitConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { useEffect, useRef, useId } from 'react';

interface ExitConfirmModalProps {
message: string;
onStay: () => void;
onExit: () => void;
}

export const ExitConfirmModal: React.FC<ExitConfirmModalProps> = ({ message, onStay, onExit }) => {
const dialogRef = useRef<HTMLDivElement>(null);
const stayButtonRef = useRef<HTMLButtonElement>(null);
const onStayRef = useRef(onStay);
onStayRef.current = onStay;
const titleId = useId();

useEffect(() => {
const previouslyFocused = document.activeElement as HTMLElement | null;
stayButtonRef.current?.focus();

const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
onStayRef.current();
return;
}
if (e.key !== 'Tab') return;
const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])',
);
if (!focusable || focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
};

document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
previouslyFocused?.focus?.();
};
}, []);

return (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'rgba(0,0,0,0.5)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 999,
backdropFilter: 'blur(8px)',
}}
>
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
className="glass"
style={{ padding: '30px', width: '320px', maxWidth: '90vw', textAlign: 'center' }}
>
<h2 id={titleId}>Confirm Exit</h2>
<p>{message}</p>
<div style={{ display: 'flex', justifyContent: 'center', gap: '16px', marginTop: '20px' }}>
<button ref={stayButtonRef} className="btn-outline" onClick={onStay}>Stay</button>
<button className="btn-neon" style={{ background: 'var(--neon-red)', color: '#fff' }} onClick={onExit}>Exit</button>
</div>
</div>
</div>
);
};

export default ExitConfirmModal;
59 changes: 6 additions & 53 deletions src/components/WorkoutScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { useWorkoutSync } from '../hooks/useWorkoutSync';
import { useDisplayConfig } from '../hooks/useDisplayConfig';
import { audioFeedbackService } from '../services/audioFeedbackService';
import { ExitConfirmModal } from './ExitConfirmModal';
import { useWorkoutWebSocket } from '../hooks/useWorkoutWebSocket';
import { useOffscreenCanvas } from '../hooks/useOffscreenCanvas';
import { injuryRiskEngine } from '../services/injuryRiskEngine';
Expand Down Expand Up @@ -1032,7 +1033,7 @@
)}
{poseService.isFallbackMode && (
<div style={{ position: "absolute", top: "20px", left: "50%", transform: "translateX(-50%)", background: "rgba(239, 68, 68, 0.9)", color: "#fff", padding: "8px 16px", borderRadius: "20px", fontSize: "12px", fontWeight: "bold", zIndex: 1100, display: "flex", alignItems: "center", gap: "8px", boxShadow: "0 4px 12px rgba(0,0,0,0.5)", whiteSpace: "nowrap" }}>
<ShieldAlert size={16} />

Check failure on line 1036 in src/components/WorkoutScreen.tsx

View workflow job for this annotation

GitHub Actions / build-and-test (22.x)

'ShieldAlert' is not defined
<span>SIMULATED FALLBACK MODE (WEBGL UNSUPPORTED)</span>
</div>
)}
Expand Down Expand Up @@ -1924,59 +1925,11 @@
`}</style>

{showExitModal && (
<div
style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
background: 'rgba(0,0,0,0.6)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
zIndex: 999,
backdropFilter: 'blur(8px)'
}}
>
<div
style={{
background: 'var(--bg-card)',
border: '1px solid rgba(255,255,255,0.2)',
borderRadius: '20px',
padding: '30px',
width: '320px',
textAlign: 'center',
color: 'white',
boxShadow: '0 8px 32px rgba(0,0,0,0.3)'
}}
>
<h2>Confirm Exit</h2>
<p>Are you sure you want to end your workout session?</p>
<div
style={{
display: 'flex',
justifyContent: 'center',
gap: '20px',
marginTop: '20px'
}}
>
<button
className="btn-neon"
onClick={() => setShowExitModal(false)}
>
Stay
</button>
<button
className="btn-neon"
style={{ background: 'var(--neon-red)' }}
onClick={handleEnd}
>
Exit
</button>
</div>
</div>
</div>
<ExitConfirmModal
message="Are you sure you want to end your workout session?"
onStay={() => setShowExitModal(false)}
onExit={handleEnd}
/>
)}
</CameraErrorBoundary>
</div>
Expand Down
Loading