Refactor mobile detection to use built-in Nuxt device module
Some checks failed
Build And Push Image / docker (push) Failing after 2m27s

Replace custom useMobileDetection composable with Nuxt's useDevice(),
removing reactive mobile detection in favor of static detection to
prevent reload loops and simplify viewport handling
This commit is contained in:
2025-08-10 14:38:02 +02:00
parent fd08c38ade
commit 2eaf9cda95
10 changed files with 532 additions and 462 deletions

View File

@@ -1,174 +0,0 @@
/**
* Unified Mobile Detection Composable
* Provides consistent mobile detection across the app
*/
export interface MobileDetectionState {
isMobile: boolean;
isSafari: boolean;
isMobileSafari: boolean;
isIOS: boolean;
isAndroid: boolean;
safariVersion?: number;
viewportHeight: number;
isInitialized: boolean;
}
// Global state to ensure single source of truth
const globalState = reactive<MobileDetectionState>({
isMobile: false,
isSafari: false,
isMobileSafari: false,
isIOS: false,
isAndroid: false,
safariVersion: undefined,
viewportHeight: 0,
isInitialized: false
});
// Track if listeners are already set up
let listenersSetup = false;
export const useMobileDetection = () => {
// Initialize detection on first use
if (!globalState.isInitialized && typeof window !== 'undefined') {
detectDevice();
globalState.isInitialized = true;
// Set up listeners only once globally
if (!listenersSetup) {
setupListeners();
listenersSetup = true;
}
}
// Return readonly state to prevent external modifications
return readonly(globalState);
};
function detectDevice() {
if (typeof window === 'undefined') return;
const userAgent = navigator.userAgent;
// Detect mobile
globalState.isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
// Detect iOS
globalState.isIOS = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
// Detect Android
globalState.isAndroid = /Android/i.test(userAgent);
// Detect Safari
globalState.isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
// Detect Mobile Safari specifically
globalState.isMobileSafari = globalState.isIOS && globalState.isSafari;
// Extract Safari version
if (globalState.isSafari) {
const match = userAgent.match(/Version\/(\d+)/);
if (match) {
globalState.safariVersion = parseInt(match[1]);
}
}
// Set initial viewport height
globalState.viewportHeight = window.innerHeight;
}
function setupListeners() {
if (typeof window === 'undefined') return;
let resizeTimeout: NodeJS.Timeout;
let lastHeight = window.innerHeight;
let initialHeight = window.innerHeight;
let keyboardOpen = false;
const handleResize = () => {
// Don't handle resize if we're in the middle of a transition
if (document.hidden) return;
clearTimeout(resizeTimeout);
// Debounce with longer delay for mobile
const debounceDelay = globalState.isMobile ? 300 : 150;
resizeTimeout = setTimeout(() => {
const newHeight = window.innerHeight;
const heightDiff = newHeight - lastHeight;
const absoluteDiff = Math.abs(heightDiff);
// Detect keyboard open/close on mobile
if (globalState.isMobile) {
// Keyboard likely opened (viewport got smaller)
if (heightDiff < -100 && newHeight < initialHeight * 0.75) {
keyboardOpen = true;
// Don't trigger viewport updates for keyboard changes
return;
}
// Keyboard likely closed (viewport got bigger)
if (heightDiff > 100 && keyboardOpen) {
keyboardOpen = false;
// Don't trigger viewport updates for keyboard changes
return;
}
}
// Only update for significant non-keyboard changes (more than 50px)
// or orientation changes (height differs significantly from initial)
const isOrientationChange = absoluteDiff > initialHeight * 0.3;
const isSignificantChange = absoluteDiff > 50;
if (isOrientationChange || (isSignificantChange && !keyboardOpen)) {
lastHeight = newHeight;
globalState.viewportHeight = newHeight;
// Update CSS custom property for mobile Safari
if (globalState.isMobileSafari) {
const vh = newHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
// Update initial height after orientation change
if (isOrientationChange) {
initialHeight = newHeight;
}
}
}, debounceDelay);
};
// Add resize listener with passive option for better performance
window.addEventListener('resize', handleResize, { passive: true });
// Also listen for orientation changes on mobile
if (globalState.isMobile) {
window.addEventListener('orientationchange', () => {
// Reset keyboard state on orientation change
keyboardOpen = false;
// Wait for orientation change to complete
setTimeout(handleResize, 200);
});
}
// Clean up on app unmount (if needed)
if (typeof window !== 'undefined') {
const cleanup = () => {
window.removeEventListener('resize', handleResize);
if (globalState.isMobile) {
window.removeEventListener('orientationchange', handleResize);
}
};
// Store cleanup function for manual cleanup if needed
(window as any).__mobileDetectionCleanup = cleanup;
}
}
// Export helper functions
export const isMobileSafari = () => globalState.isMobileSafari;
export const isMobile = () => globalState.isMobile;
export const isIOS = () => globalState.isIOS;
export const getViewportHeight = () => globalState.viewportHeight;