Replace date-fns with native date formatting and remove unused code
All checks were successful
Build And Push Image / docker (push) Successful in 1m34s
All checks were successful
Build And Push Image / docker (push) Successful in 1m34s
Remove date-fns dependency in favor of native Intl.DateTimeFormat APIs, clean up obsolete admin endpoints, utility files, and archived documentation. Consolidate docs structure and remove unused plugins.
This commit is contained in:
@@ -1,214 +0,0 @@
|
||||
// Mobile detection and debugging utilities
|
||||
|
||||
export interface DeviceInfo {
|
||||
isMobile: boolean;
|
||||
isIOS: boolean;
|
||||
isAndroid: boolean;
|
||||
isTablet: boolean;
|
||||
browser: string;
|
||||
version: string;
|
||||
userAgent: string;
|
||||
cookieEnabled: boolean;
|
||||
screenWidth: number;
|
||||
screenHeight: number;
|
||||
}
|
||||
|
||||
export function getDeviceInfo(): DeviceInfo {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
isMobile: false,
|
||||
isIOS: false,
|
||||
isAndroid: false,
|
||||
isTablet: false,
|
||||
browser: 'Unknown',
|
||||
version: 'Unknown',
|
||||
userAgent: 'Server',
|
||||
cookieEnabled: false,
|
||||
screenWidth: 0,
|
||||
screenHeight: 0
|
||||
};
|
||||
}
|
||||
|
||||
const ua = navigator.userAgent;
|
||||
|
||||
// Mobile detection
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
|
||||
const isIOS = /iPad|iPhone|iPod/.test(ua);
|
||||
const isAndroid = /Android/.test(ua);
|
||||
const isTablet = /iPad|Android(?=.*\bMobile\b)(?!.*\bMobile\b)/i.test(ua);
|
||||
|
||||
// Browser detection
|
||||
let browser = 'Unknown';
|
||||
let version = 'Unknown';
|
||||
|
||||
if (ua.includes('Chrome') && !ua.includes('Edge')) {
|
||||
browser = 'Chrome';
|
||||
const match = ua.match(/Chrome\/(\d+)/);
|
||||
version = match ? match[1] : 'Unknown';
|
||||
} else if (ua.includes('Safari') && !ua.includes('Chrome')) {
|
||||
browser = 'Safari';
|
||||
const match = ua.match(/Version\/(\d+)/);
|
||||
version = match ? match[1] : 'Unknown';
|
||||
} else if (ua.includes('Firefox')) {
|
||||
browser = 'Firefox';
|
||||
const match = ua.match(/Firefox\/(\d+)/);
|
||||
version = match ? match[1] : 'Unknown';
|
||||
} else if (ua.includes('Edge')) {
|
||||
browser = 'Edge';
|
||||
const match = ua.match(/Edge\/(\d+)/);
|
||||
version = match ? match[1] : 'Unknown';
|
||||
}
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
isIOS,
|
||||
isAndroid,
|
||||
isTablet,
|
||||
browser,
|
||||
version,
|
||||
userAgent: ua,
|
||||
cookieEnabled: navigator.cookieEnabled,
|
||||
screenWidth: window.screen.width,
|
||||
screenHeight: window.screen.height
|
||||
};
|
||||
}
|
||||
|
||||
export function logDeviceInfo(label: string = 'Device Info'): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const info = getDeviceInfo();
|
||||
|
||||
console.group(`📱 ${label}`);
|
||||
console.log('Mobile:', info.isMobile);
|
||||
console.log('iOS:', info.isIOS);
|
||||
console.log('Android:', info.isAndroid);
|
||||
console.log('Tablet:', info.isTablet);
|
||||
console.log('Browser:', `${info.browser} ${info.version}`);
|
||||
console.log('Cookies Enabled:', info.cookieEnabled);
|
||||
console.log('Screen:', `${info.screenWidth}x${info.screenHeight}`);
|
||||
console.log('User Agent:', info.userAgent);
|
||||
|
||||
// Additional debugging info
|
||||
console.log('Viewport:', `${window.innerWidth}x${window.innerHeight}`);
|
||||
console.log('Device Pixel Ratio:', window.devicePixelRatio);
|
||||
console.log('Online:', navigator.onLine);
|
||||
console.log('Language:', navigator.language);
|
||||
console.log('Platform:', navigator.platform);
|
||||
|
||||
// Cookie testing
|
||||
try {
|
||||
document.cookie = 'test=1; path=/';
|
||||
const canSetCookie = document.cookie.includes('test=1');
|
||||
console.log('Can Set Cookies:', canSetCookie);
|
||||
if (canSetCookie) {
|
||||
document.cookie = 'test=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Cookie Test Error:', error);
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
export function isMobileDevice(): boolean {
|
||||
return getDeviceInfo().isMobile;
|
||||
}
|
||||
|
||||
export function isIOSDevice(): boolean {
|
||||
return getDeviceInfo().isIOS;
|
||||
}
|
||||
|
||||
export function isAndroidDevice(): boolean {
|
||||
return getDeviceInfo().isAndroid;
|
||||
}
|
||||
|
||||
export function getMobileBrowser(): string {
|
||||
const info = getDeviceInfo();
|
||||
return `${info.browser} ${info.version}`;
|
||||
}
|
||||
|
||||
// Enhanced mobile login debugging
|
||||
export function debugMobileLogin(context: string): void {
|
||||
if (!isMobileDevice()) return;
|
||||
|
||||
console.group(`🔐 Mobile Login Debug - ${context}`);
|
||||
logDeviceInfo('Current Device');
|
||||
|
||||
// Check for known mobile issues
|
||||
const info = getDeviceInfo();
|
||||
const issues: string[] = [];
|
||||
|
||||
if (info.isIOS && info.browser === 'Safari' && parseInt(info.version) < 14) {
|
||||
issues.push('Safari < 14: Cookie issues with SameSite');
|
||||
}
|
||||
|
||||
if (info.isAndroid && info.browser === 'Chrome' && parseInt(info.version) < 80) {
|
||||
issues.push('Chrome < 80: SameSite cookie support limited');
|
||||
}
|
||||
|
||||
if (!info.cookieEnabled) {
|
||||
issues.push('Cookies disabled in browser');
|
||||
}
|
||||
|
||||
if (info.screenWidth < 375) {
|
||||
issues.push('Small screen may affect layout');
|
||||
}
|
||||
|
||||
if (issues.length > 0) {
|
||||
console.warn('⚠️ Potential Issues:', issues);
|
||||
} else {
|
||||
console.log('✅ No known compatibility issues detected');
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
// Network debugging
|
||||
export function debugNetworkConditions(): void {
|
||||
if (typeof navigator === 'undefined') return;
|
||||
|
||||
console.group('🌐 Network Conditions');
|
||||
console.log('Online:', navigator.onLine);
|
||||
|
||||
// @ts-ignore - connection API is experimental
|
||||
if (navigator.connection) {
|
||||
// @ts-ignore
|
||||
const conn = navigator.connection;
|
||||
console.log('Connection Type:', conn.effectiveType);
|
||||
console.log('Downlink:', conn.downlink, 'Mbps');
|
||||
console.log('RTT:', conn.rtt, 'ms');
|
||||
console.log('Save Data:', conn.saveData);
|
||||
} else {
|
||||
console.log('Network API not available');
|
||||
}
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
// PWA debugging
|
||||
export function debugPWACapabilities(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
console.group('📱 PWA Capabilities');
|
||||
console.log('Service Worker:', 'serviceWorker' in navigator);
|
||||
console.log('Web App Manifest:', 'onbeforeinstallprompt' in window);
|
||||
console.log('Standalone Mode:', window.matchMedia('(display-mode: standalone)').matches);
|
||||
console.log('Full Screen API:', 'requestFullscreen' in document.documentElement);
|
||||
console.log('Notification API:', 'Notification' in window);
|
||||
console.log('Push API:', 'PushManager' in window);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
// Complete mobile debugging suite
|
||||
export function runMobileDiagnostics(): void {
|
||||
if (!isMobileDevice()) {
|
||||
console.log('📱 Not a mobile device, skipping mobile diagnostics');
|
||||
return;
|
||||
}
|
||||
|
||||
console.group('🔍 Mobile Diagnostics Suite');
|
||||
logDeviceInfo();
|
||||
debugNetworkConditions();
|
||||
debugPWACapabilities();
|
||||
console.groupEnd();
|
||||
}
|
||||
@@ -1,310 +0,0 @@
|
||||
/**
|
||||
* Reload Loop Detection and Prevention Utility
|
||||
* Advanced mobile Safari reload loop prevention system
|
||||
*/
|
||||
|
||||
interface PageLoadInfo {
|
||||
url: string;
|
||||
timestamp: number;
|
||||
userAgent: string;
|
||||
loadCount: number;
|
||||
}
|
||||
|
||||
interface ReloadLoopState {
|
||||
enabled: boolean;
|
||||
pageLoads: PageLoadInfo[];
|
||||
blockedPages: Set<string>;
|
||||
emergencyMode: boolean;
|
||||
debugMode: boolean;
|
||||
}
|
||||
|
||||
const RELOAD_LOOP_THRESHOLD = 5; // Max page loads in time window
|
||||
const TIME_WINDOW = 10000; // 10 seconds
|
||||
const EMERGENCY_BLOCK_TIME = 30000; // 30 seconds
|
||||
const STORAGE_KEY = 'reload_loop_prevention';
|
||||
|
||||
/**
|
||||
* Get or initialize reload loop state
|
||||
*/
|
||||
function getReloadLoopState(): ReloadLoopState {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
enabled: false,
|
||||
pageLoads: [],
|
||||
blockedPages: new Set(),
|
||||
emergencyMode: false,
|
||||
debugMode: false
|
||||
};
|
||||
}
|
||||
|
||||
// Use sessionStorage for persistence across reloads
|
||||
try {
|
||||
const stored = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
return {
|
||||
...parsed,
|
||||
blockedPages: new Set(parsed.blockedPages || [])
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[reload-prevention] Failed to parse stored state:', error);
|
||||
}
|
||||
|
||||
// Initialize new state
|
||||
const initialState: ReloadLoopState = {
|
||||
enabled: true,
|
||||
pageLoads: [],
|
||||
blockedPages: new Set(),
|
||||
emergencyMode: false,
|
||||
debugMode: process.env.NODE_ENV === 'development'
|
||||
};
|
||||
|
||||
saveReloadLoopState(initialState);
|
||||
return initialState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save reload loop state to sessionStorage
|
||||
*/
|
||||
function saveReloadLoopState(state: ReloadLoopState): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
const toStore = {
|
||||
...state,
|
||||
blockedPages: Array.from(state.blockedPages)
|
||||
};
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
|
||||
} catch (error) {
|
||||
console.warn('[reload-prevention] Failed to save state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a page load and check for reload loops
|
||||
*/
|
||||
export function recordPageLoad(url: string): boolean {
|
||||
const state = getReloadLoopState();
|
||||
|
||||
if (!state.enabled) {
|
||||
return true; // Allow load
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const userAgent = navigator.userAgent || '';
|
||||
|
||||
// Clean old page loads
|
||||
state.pageLoads = state.pageLoads.filter(
|
||||
load => now - load.timestamp < TIME_WINDOW
|
||||
);
|
||||
|
||||
// Find existing load for this URL
|
||||
const existingLoad = state.pageLoads.find(load => load.url === url);
|
||||
|
||||
if (existingLoad) {
|
||||
existingLoad.loadCount++;
|
||||
existingLoad.timestamp = now;
|
||||
} else {
|
||||
state.pageLoads.push({
|
||||
url,
|
||||
timestamp: now,
|
||||
userAgent,
|
||||
loadCount: 1
|
||||
});
|
||||
}
|
||||
|
||||
// Check for reload loop
|
||||
const urlLoads = state.pageLoads.filter(load => load.url === url);
|
||||
const totalLoads = urlLoads.reduce((sum, load) => sum + load.loadCount, 0);
|
||||
|
||||
if (totalLoads >= RELOAD_LOOP_THRESHOLD) {
|
||||
console.error(`[reload-prevention] Reload loop detected for ${url} (${totalLoads} loads)`);
|
||||
|
||||
// Block this page
|
||||
state.blockedPages.add(url);
|
||||
state.emergencyMode = true;
|
||||
|
||||
// Set emergency timeout
|
||||
setTimeout(() => {
|
||||
state.emergencyMode = false;
|
||||
state.blockedPages.delete(url);
|
||||
saveReloadLoopState(state);
|
||||
console.log(`[reload-prevention] Emergency block lifted for ${url}`);
|
||||
}, EMERGENCY_BLOCK_TIME);
|
||||
|
||||
saveReloadLoopState(state);
|
||||
return false; // Block load
|
||||
}
|
||||
|
||||
saveReloadLoopState(state);
|
||||
return true; // Allow load
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a page is currently blocked
|
||||
*/
|
||||
export function isPageBlocked(url: string): boolean {
|
||||
const state = getReloadLoopState();
|
||||
return state.blockedPages.has(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize reload loop prevention for a page
|
||||
*/
|
||||
export function initReloadLoopPrevention(pageName: string): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentUrl = window.location.pathname;
|
||||
const canLoad = recordPageLoad(currentUrl);
|
||||
|
||||
if (!canLoad) {
|
||||
console.error(`[reload-prevention] Page load blocked: ${pageName} (${currentUrl})`);
|
||||
|
||||
// Show emergency message
|
||||
showEmergencyMessage(pageName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getReloadLoopState().debugMode) {
|
||||
console.log(`[reload-prevention] Page load allowed: ${pageName} (${currentUrl})`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show emergency message when page is blocked
|
||||
*/
|
||||
function showEmergencyMessage(pageName: string): void {
|
||||
const message = `
|
||||
<div style="
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(163, 21, 21, 0.95);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10000;
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
">
|
||||
<h1 style="font-size: 24px; margin-bottom: 20px;">Page Loading Temporarily Blocked</h1>
|
||||
<p style="font-size: 16px; margin-bottom: 20px;">
|
||||
Multiple rapid page loads detected for ${pageName}.<br>
|
||||
This is a safety measure to prevent infinite loading loops.
|
||||
</p>
|
||||
<p style="font-size: 14px; margin-bottom: 30px;">
|
||||
The block will be automatically lifted in 30 seconds.
|
||||
</p>
|
||||
<button onclick="location.href='/'" style="
|
||||
background: white;
|
||||
color: #a31515;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
margin-right: 15px;
|
||||
">Return to Home</button>
|
||||
<button onclick="window.history.back()" style="
|
||||
background: transparent;
|
||||
color: white;
|
||||
border: 2px solid white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
">Go Back</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.innerHTML = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear reload loop prevention state (for testing)
|
||||
*/
|
||||
export function clearReloadLoopPrevention(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
console.log('[reload-prevention] State cleared');
|
||||
} catch (error) {
|
||||
console.warn('[reload-prevention] Failed to clear state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current reload loop prevention status
|
||||
*/
|
||||
export function getReloadLoopStatus(): {
|
||||
enabled: boolean;
|
||||
emergencyMode: boolean;
|
||||
blockedPages: string[];
|
||||
pageLoads: PageLoadInfo[];
|
||||
} {
|
||||
const state = getReloadLoopState();
|
||||
return {
|
||||
enabled: state.enabled,
|
||||
emergencyMode: state.emergencyMode,
|
||||
blockedPages: Array.from(state.blockedPages),
|
||||
pageLoads: state.pageLoads
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile Safari specific optimizations
|
||||
*/
|
||||
export function applyMobileSafariReloadLoopFixes(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const userAgent = navigator.userAgent || '';
|
||||
const isMobileSafari = /iPad|iPhone|iPod/.test(userAgent) && /Safari/i.test(userAgent) && !/Chrome/i.test(userAgent);
|
||||
|
||||
if (!isMobileSafari) return;
|
||||
|
||||
console.log('[reload-prevention] Applying mobile Safari specific fixes');
|
||||
|
||||
// Prevent Safari's aggressive caching that can cause reload loops
|
||||
window.addEventListener('pageshow', (event) => {
|
||||
if (event.persisted) {
|
||||
console.log('[reload-prevention] Page restored from bfcache');
|
||||
// Force a small delay to prevent immediate re-execution
|
||||
setTimeout(() => {
|
||||
console.log('[reload-prevention] bfcache restore handled');
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Safari's navigation timing issues
|
||||
window.addEventListener('beforeunload', () => {
|
||||
// Mark navigation in progress to prevent reload loop detection
|
||||
const state = getReloadLoopState();
|
||||
state.enabled = false;
|
||||
saveReloadLoopState(state);
|
||||
});
|
||||
|
||||
// Re-enable after navigation completes
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
const state = getReloadLoopState();
|
||||
state.enabled = true;
|
||||
saveReloadLoopState(state);
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-initialize on module load
|
||||
if (typeof window !== 'undefined') {
|
||||
applyMobileSafariReloadLoopFixes();
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
/**
|
||||
* Static Device Detection Utility
|
||||
* Provides non-reactive device detection for Safari iOS reload loop prevention
|
||||
* Uses direct navigator.userAgent analysis without creating Vue reactive dependencies
|
||||
*/
|
||||
|
||||
export interface DeviceInfo {
|
||||
isMobile: boolean;
|
||||
isIos: boolean;
|
||||
isSafari: boolean;
|
||||
isMobileSafari: boolean;
|
||||
isAndroid: boolean;
|
||||
userAgent: string;
|
||||
}
|
||||
|
||||
let cachedDeviceInfo: DeviceInfo | null = null;
|
||||
|
||||
/**
|
||||
* Get static device information without creating reactive dependencies
|
||||
* Results are cached to prevent multiple userAgent parsing
|
||||
*/
|
||||
export function getStaticDeviceInfo(): DeviceInfo {
|
||||
// Return cached result if available
|
||||
if (cachedDeviceInfo) {
|
||||
return cachedDeviceInfo;
|
||||
}
|
||||
|
||||
// Only run on client-side
|
||||
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
||||
cachedDeviceInfo = {
|
||||
isMobile: false,
|
||||
isIos: false,
|
||||
isSafari: false,
|
||||
isMobileSafari: false,
|
||||
isAndroid: false,
|
||||
userAgent: ''
|
||||
};
|
||||
return cachedDeviceInfo;
|
||||
}
|
||||
|
||||
const userAgent = navigator.userAgent;
|
||||
|
||||
// Device detection logic
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
||||
const isIos = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
|
||||
const isAndroid = /Android/i.test(userAgent);
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
|
||||
const isMobileSafari = isIos && isSafari;
|
||||
|
||||
// Cache the result
|
||||
cachedDeviceInfo = {
|
||||
isMobile,
|
||||
isIos,
|
||||
isSafari,
|
||||
isMobileSafari,
|
||||
isAndroid,
|
||||
userAgent
|
||||
};
|
||||
|
||||
return cachedDeviceInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get CSS classes for device-specific styling
|
||||
* Returns a space-separated string of CSS classes
|
||||
*/
|
||||
export function getDeviceCssClasses(baseClass: string = ''): string {
|
||||
const device = getStaticDeviceInfo();
|
||||
const classes = [baseClass].filter(Boolean);
|
||||
|
||||
if (device.isMobile) classes.push('is-mobile');
|
||||
if (device.isIos) classes.push('is-ios');
|
||||
if (device.isSafari) classes.push('is-safari');
|
||||
if (device.isMobileSafari) classes.push('is-mobile-safari');
|
||||
if (device.isAndroid) classes.push('is-android');
|
||||
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current device is mobile Safari specifically
|
||||
* This is the primary problematic browser for reload loops
|
||||
*/
|
||||
export function isMobileSafari(): boolean {
|
||||
return getStaticDeviceInfo().isMobileSafari;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply mobile Safari specific optimizations to DOM element
|
||||
* Should be called once per component to prevent reactive updates
|
||||
*/
|
||||
export function applyMobileSafariOptimizations(element?: HTMLElement): void {
|
||||
if (!isMobileSafari()) return;
|
||||
|
||||
const targetElement = element || document.documentElement;
|
||||
|
||||
// Apply performance optimization classes
|
||||
targetElement.classList.add('is-mobile-safari', 'performance-optimized');
|
||||
|
||||
// Set viewport height CSS variable for mobile Safari
|
||||
const vh = window.innerHeight * 0.01;
|
||||
targetElement.style.setProperty('--vh', `${vh}px`);
|
||||
|
||||
// Disable problematic CSS features for performance
|
||||
targetElement.style.setProperty('--backdrop-filter', 'none');
|
||||
targetElement.style.setProperty('--will-change', 'auto');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get viewport meta content optimized for mobile Safari
|
||||
*/
|
||||
export function getMobileSafariViewportMeta(): string {
|
||||
const device = getStaticDeviceInfo();
|
||||
|
||||
if (device.isMobileSafari) {
|
||||
// Prevent zoom on input focus for iOS Safari
|
||||
return 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
|
||||
}
|
||||
|
||||
return 'width=device-width, initial-scale=1.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear cached device info (useful for testing)
|
||||
*/
|
||||
export function clearDeviceInfoCache(): void {
|
||||
cachedDeviceInfo = null;
|
||||
}
|
||||
@@ -1,300 +0,0 @@
|
||||
/**
|
||||
* Client-side verification state management with circuit breaker pattern
|
||||
* Prevents endless reload loops on mobile browsers
|
||||
*/
|
||||
|
||||
export interface VerificationAttempt {
|
||||
token: string;
|
||||
attempts: number;
|
||||
lastAttempt: number;
|
||||
maxAttempts: number;
|
||||
status: 'pending' | 'success' | 'failed' | 'blocked';
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'email_verification_state';
|
||||
const MAX_ATTEMPTS_DEFAULT = 3;
|
||||
const ATTEMPT_WINDOW = 5 * 60 * 1000; // 5 minutes
|
||||
const CIRCUIT_BREAKER_TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
/**
|
||||
* Get verification state for a token
|
||||
*/
|
||||
export function getVerificationState(token: string): VerificationAttempt | null {
|
||||
if (typeof window === 'undefined' || !token) return null;
|
||||
|
||||
try {
|
||||
const stored = sessionStorage.getItem(`${STORAGE_KEY}_${token.substring(0, 10)}`);
|
||||
if (!stored) return null;
|
||||
|
||||
const state = JSON.parse(stored) as VerificationAttempt;
|
||||
|
||||
// Check if circuit breaker timeout has passed
|
||||
const now = Date.now();
|
||||
if (state.status === 'blocked' && (now - state.lastAttempt) > CIRCUIT_BREAKER_TIMEOUT) {
|
||||
console.log('[verification-state] Circuit breaker timeout passed, resetting state');
|
||||
clearVerificationState(token);
|
||||
return null;
|
||||
}
|
||||
|
||||
return state;
|
||||
} catch (error) {
|
||||
console.warn('[verification-state] Failed to parse stored state:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize or update verification state
|
||||
*/
|
||||
export function initVerificationState(token: string, maxAttempts: number = MAX_ATTEMPTS_DEFAULT): VerificationAttempt {
|
||||
if (typeof window === 'undefined' || !token) {
|
||||
throw new Error('Cannot initialize verification state: no window or token');
|
||||
}
|
||||
|
||||
const existing = getVerificationState(token);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const state: VerificationAttempt = {
|
||||
token,
|
||||
attempts: 0,
|
||||
lastAttempt: 0,
|
||||
maxAttempts,
|
||||
status: 'pending',
|
||||
errors: []
|
||||
};
|
||||
|
||||
try {
|
||||
sessionStorage.setItem(`${STORAGE_KEY}_${token.substring(0, 10)}`, JSON.stringify(state));
|
||||
console.log('[verification-state] Initialized verification state for token');
|
||||
return state;
|
||||
} catch (error) {
|
||||
console.error('[verification-state] Failed to save state:', error);
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a verification attempt
|
||||
*/
|
||||
export function recordAttempt(token: string, success: boolean = false, error?: string): VerificationAttempt {
|
||||
if (typeof window === 'undefined' || !token) {
|
||||
throw new Error('Cannot record attempt: no window or token');
|
||||
}
|
||||
|
||||
const state = getVerificationState(token) || initVerificationState(token);
|
||||
const now = Date.now();
|
||||
|
||||
// Check if we're within the attempt window
|
||||
if (state.lastAttempt > 0 && (now - state.lastAttempt) > ATTEMPT_WINDOW) {
|
||||
console.log('[verification-state] Attempt window expired, resetting counter');
|
||||
state.attempts = 0;
|
||||
state.errors = [];
|
||||
}
|
||||
|
||||
state.attempts++;
|
||||
state.lastAttempt = now;
|
||||
|
||||
if (success) {
|
||||
state.status = 'success';
|
||||
console.log('[verification-state] Verification successful, clearing state');
|
||||
// Don't clear immediately - let the navigation complete first
|
||||
setTimeout(() => clearVerificationState(token), 1000);
|
||||
} else {
|
||||
if (error) {
|
||||
state.errors.push(error);
|
||||
}
|
||||
|
||||
if (state.attempts >= state.maxAttempts) {
|
||||
state.status = 'blocked';
|
||||
console.log(`[verification-state] Maximum attempts (${state.maxAttempts}) reached, blocking further attempts`);
|
||||
} else {
|
||||
state.status = 'failed';
|
||||
console.log(`[verification-state] Attempt ${state.attempts}/${state.maxAttempts} failed`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
sessionStorage.setItem(`${STORAGE_KEY}_${token.substring(0, 10)}`, JSON.stringify(state));
|
||||
} catch (error) {
|
||||
console.error('[verification-state] Failed to update state:', error);
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if verification should be blocked
|
||||
*/
|
||||
export function shouldBlockVerification(token: string): boolean {
|
||||
if (typeof window === 'undefined' || !token) return false;
|
||||
|
||||
const state = getVerificationState(token);
|
||||
if (!state) return false;
|
||||
|
||||
if (state.status === 'blocked') {
|
||||
const timeRemaining = CIRCUIT_BREAKER_TIMEOUT - (Date.now() - state.lastAttempt);
|
||||
if (timeRemaining > 0) {
|
||||
console.log(`[verification-state] Verification blocked for ${Math.ceil(timeRemaining / 1000 / 60)} more minutes`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return state.status === 'success' || (state.attempts >= state.maxAttempts && state.status !== 'pending');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear verification state for a token
|
||||
*/
|
||||
export function clearVerificationState(token: string): void {
|
||||
if (typeof window === 'undefined' || !token) return;
|
||||
|
||||
try {
|
||||
sessionStorage.removeItem(`${STORAGE_KEY}_${token.substring(0, 10)}`);
|
||||
console.log('[verification-state] Cleared verification state');
|
||||
} catch (error) {
|
||||
console.warn('[verification-state] Failed to clear state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user-friendly status message
|
||||
*/
|
||||
export function getStatusMessage(state: VerificationAttempt | null): string {
|
||||
if (!state) return '';
|
||||
|
||||
switch (state.status) {
|
||||
case 'pending':
|
||||
return '';
|
||||
case 'success':
|
||||
return 'Email verified successfully!';
|
||||
case 'failed':
|
||||
if (state.attempts === 1) {
|
||||
return 'Verification failed. Retrying...';
|
||||
}
|
||||
return `Verification failed (${state.attempts}/${state.maxAttempts} attempts). ${state.maxAttempts - state.attempts} attempts remaining.`;
|
||||
case 'blocked':
|
||||
const timeRemaining = Math.ceil((CIRCUIT_BREAKER_TIMEOUT - (Date.now() - state.lastAttempt)) / 1000 / 60);
|
||||
return `Too many failed attempts. Please wait ${timeRemaining} minutes before trying again, or contact support.`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Progressive navigation with fallbacks for mobile browsers
|
||||
*/
|
||||
export async function navigateWithFallback(url: string, options: { replace?: boolean } = {}): Promise<boolean> {
|
||||
if (typeof window === 'undefined') return false;
|
||||
|
||||
console.log(`[verification-state] Attempting navigation to: ${url}`);
|
||||
|
||||
try {
|
||||
// Method 1: Use Nuxt navigateTo
|
||||
if (typeof navigateTo === 'function') {
|
||||
console.log('[verification-state] Using navigateTo');
|
||||
await navigateTo(url, options);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[verification-state] navigateTo failed:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
// Method 2: Use Vue Router (if available)
|
||||
const nuxtApp = (window as any)?.$nuxt;
|
||||
if (nuxtApp?.$router) {
|
||||
console.log('[verification-state] Using Vue Router');
|
||||
if (options.replace) {
|
||||
await nuxtApp.$router.replace(url);
|
||||
} else {
|
||||
await nuxtApp.$router.push(url);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[verification-state] Vue Router failed:', error);
|
||||
}
|
||||
|
||||
// Method 3: Direct window.location (mobile fallback)
|
||||
console.log('[verification-state] Using window.location fallback');
|
||||
if (options.replace) {
|
||||
window.location.replace(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile-specific delay before navigation to ensure stability
|
||||
*/
|
||||
export function getMobileNavigationDelay(): number {
|
||||
if (typeof window === 'undefined') return 0;
|
||||
|
||||
// Detect mobile browsers
|
||||
const isMobile = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
||||
const isSafari = /Safari/i.test(navigator.userAgent) && !/Chrome/i.test(navigator.userAgent);
|
||||
|
||||
if (isMobile && isSafari) {
|
||||
return 500; // Extra delay for Safari on iOS
|
||||
} else if (isMobile) {
|
||||
return 300; // Standard mobile delay
|
||||
}
|
||||
|
||||
return 100; // Minimal delay for desktop
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up all expired verification states
|
||||
*/
|
||||
export function cleanupExpiredStates(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
const now = Date.now();
|
||||
const keysToRemove: string[] = [];
|
||||
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
const key = sessionStorage.key(i);
|
||||
if (!key?.startsWith(STORAGE_KEY)) continue;
|
||||
|
||||
try {
|
||||
const stored = sessionStorage.getItem(key);
|
||||
if (!stored) continue;
|
||||
|
||||
const state = JSON.parse(stored) as VerificationAttempt;
|
||||
|
||||
// Remove states older than circuit breaker timeout
|
||||
if ((now - state.lastAttempt) > CIRCUIT_BREAKER_TIMEOUT) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
} catch (error) {
|
||||
// Remove invalid stored data
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach(key => {
|
||||
sessionStorage.removeItem(key);
|
||||
});
|
||||
|
||||
if (keysToRemove.length > 0) {
|
||||
console.log(`[verification-state] Cleaned up ${keysToRemove.length} expired verification states`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[verification-state] Failed to cleanup expired states:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-cleanup on page load
|
||||
if (typeof window !== 'undefined') {
|
||||
// Clean up immediately
|
||||
cleanupExpiredStates();
|
||||
|
||||
// Clean up periodically
|
||||
setInterval(cleanupExpiredStates, 5 * 60 * 1000); // Every 5 minutes
|
||||
}
|
||||
@@ -1,142 +0,0 @@
|
||||
/**
|
||||
* CSS-Only Viewport Management System
|
||||
* Handles mobile Safari viewport height changes through CSS custom properties only,
|
||||
* without triggering any Vue component reactivity.
|
||||
*/
|
||||
|
||||
class ViewportManager {
|
||||
private static instance: ViewportManager;
|
||||
private initialized = false;
|
||||
private resizeTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
static getInstance(): ViewportManager {
|
||||
if (!ViewportManager.instance) {
|
||||
ViewportManager.instance = new ViewportManager();
|
||||
}
|
||||
return ViewportManager.instance;
|
||||
}
|
||||
|
||||
init() {
|
||||
if (this.initialized || typeof window === 'undefined') return;
|
||||
|
||||
console.log('[ViewportManager] Initializing CSS-only viewport management');
|
||||
|
||||
// Static device detection (no reactive dependencies)
|
||||
const userAgent = navigator.userAgent;
|
||||
const isIOS = /iPad|iPhone|iPod/.test(userAgent) && !(window as any).MSStream;
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
|
||||
const isMobileSafari = isIOS && isSafari;
|
||||
|
||||
// Only apply to mobile Safari where viewport issues occur
|
||||
if (!isMobileSafari) {
|
||||
console.log('[ViewportManager] Not mobile Safari, skipping viewport management');
|
||||
return;
|
||||
}
|
||||
|
||||
let lastHeight = window.innerHeight;
|
||||
let initialHeight = window.innerHeight;
|
||||
let keyboardOpen = false;
|
||||
|
||||
const handleResize = () => {
|
||||
// Skip if document is hidden (tab not active)
|
||||
if (document.hidden) return;
|
||||
|
||||
// Clear any existing timeout
|
||||
if (this.resizeTimeout) {
|
||||
clearTimeout(this.resizeTimeout);
|
||||
}
|
||||
|
||||
// Debounce with longer delay for mobile Safari
|
||||
this.resizeTimeout = setTimeout(() => {
|
||||
const newHeight = window.innerHeight;
|
||||
const heightDiff = newHeight - lastHeight;
|
||||
const absoluteDiff = Math.abs(heightDiff);
|
||||
|
||||
// Detect keyboard open/close patterns
|
||||
if (heightDiff < -100 && newHeight < initialHeight * 0.75) {
|
||||
keyboardOpen = true;
|
||||
console.log('[ViewportManager] Keyboard opened, skipping update');
|
||||
return;
|
||||
}
|
||||
|
||||
if (heightDiff > 100 && keyboardOpen) {
|
||||
keyboardOpen = false;
|
||||
console.log('[ViewportManager] Keyboard closed, skipping update');
|
||||
return;
|
||||
}
|
||||
|
||||
// Only update for significant non-keyboard changes
|
||||
const isOrientationChange = absoluteDiff > initialHeight * 0.3;
|
||||
const isSignificantChange = absoluteDiff > 50;
|
||||
|
||||
if (isOrientationChange || (isSignificantChange && !keyboardOpen)) {
|
||||
lastHeight = newHeight;
|
||||
|
||||
// Update CSS custom property only - no Vue reactivity
|
||||
const vh = newHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
|
||||
console.log('[ViewportManager] Updated --vh to:', `${vh}px`);
|
||||
|
||||
// Update initial height after orientation change
|
||||
if (isOrientationChange) {
|
||||
initialHeight = newHeight;
|
||||
console.log('[ViewportManager] Orientation change detected, updated initial height');
|
||||
}
|
||||
}
|
||||
}, 300); // Longer debounce for mobile Safari
|
||||
};
|
||||
|
||||
// Set initial CSS custom property
|
||||
const initialVh = initialHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${initialVh}px`);
|
||||
console.log('[ViewportManager] Set initial --vh to:', `${initialVh}px`);
|
||||
|
||||
// Add resize listener with passive option for better performance
|
||||
window.addEventListener('resize', handleResize, { passive: true });
|
||||
|
||||
// Also listen for orientation changes on mobile
|
||||
window.addEventListener('orientationchange', () => {
|
||||
keyboardOpen = false; // Reset keyboard state on orientation change
|
||||
console.log('[ViewportManager] Orientation change event, scheduling resize handler');
|
||||
// Wait for orientation change to complete
|
||||
setTimeout(handleResize, 200);
|
||||
});
|
||||
|
||||
// Add visibility change listener to pause updates when tab is hidden
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden && this.resizeTimeout) {
|
||||
clearTimeout(this.resizeTimeout);
|
||||
this.resizeTimeout = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.initialized = true;
|
||||
console.log('[ViewportManager] Initialization complete');
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.resizeTimeout) {
|
||||
clearTimeout(this.resizeTimeout);
|
||||
this.resizeTimeout = null;
|
||||
}
|
||||
this.initialized = false;
|
||||
console.log('[ViewportManager] Cleanup complete');
|
||||
}
|
||||
}
|
||||
|
||||
// Export singleton instance
|
||||
export const viewportManager = ViewportManager.getInstance();
|
||||
|
||||
// Auto-initialize on client side
|
||||
if (typeof window !== 'undefined') {
|
||||
// Initialize after DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
viewportManager.init();
|
||||
});
|
||||
} else {
|
||||
// DOM is already ready
|
||||
viewportManager.init();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user