Add Mobile Safari optimizations and fixes to signup page
Build And Push Image / docker (push) Successful in 3m10s
Details
Build And Push Image / docker (push) Successful in 3m10s
Details
- Implement device detection and performance optimization flags - Add dynamic CSS classes based on device capabilities - Create mobile safari utility functions and client plugin - Optimize backdrop filters and hardware acceleration for iOS - Fix viewport height issues with mobile Safari fallbacks - Update membership fee config and add IBAN payment details - Prevent horizontal scrolling and improve mobile UX
This commit is contained in:
parent
358e9c0ad1
commit
44cdc988ee
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div class="signup-container">
|
||||
<div :class="containerClasses">
|
||||
<v-container fluid class="fill-height">
|
||||
<v-row class="fill-height" justify="center" align="center">
|
||||
<v-col cols="12" sm="10" md="8" lg="6" xl="5">
|
||||
<v-card class="signup-card" elevation="24" :loading="loading">
|
||||
<v-card :class="cardClasses" elevation="24" :loading="loading">
|
||||
<v-card-text class="pa-8">
|
||||
<!-- Logo and Welcome -->
|
||||
<div class="text-center mb-6">
|
||||
|
|
@ -209,6 +209,14 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import type { RegistrationFormData, RecaptchaConfig, RegistrationConfig } from '~/utils/types';
|
||||
import {
|
||||
getDeviceInfo,
|
||||
needsPerformanceOptimization,
|
||||
shouldDisableBackdropFilter,
|
||||
getOptimizedClasses,
|
||||
applyMobileSafariFixes,
|
||||
debounce
|
||||
} from '~/utils/mobile-safari-utils';
|
||||
|
||||
// Declare global grecaptcha interface for TypeScript
|
||||
declare global {
|
||||
|
|
@ -225,12 +233,17 @@ definePageMeta({
|
|||
layout: false
|
||||
});
|
||||
|
||||
// Configs - need to be declared first
|
||||
// Mobile Safari optimization flags
|
||||
const deviceInfo = ref(getDeviceInfo());
|
||||
const performanceMode = ref(needsPerformanceOptimization());
|
||||
const disableBackdropFilter = ref(shouldDisableBackdropFilter());
|
||||
|
||||
// Configs with fallback defaults
|
||||
const recaptchaConfig = ref<RecaptchaConfig>({ siteKey: '', secretKey: '' });
|
||||
const registrationConfig = ref<RegistrationConfig>({
|
||||
membershipFee: 50,
|
||||
iban: '',
|
||||
accountHolder: ''
|
||||
membershipFee: 150,
|
||||
iban: 'MC58 1756 9000 0104 0050 1001 860',
|
||||
accountHolder: 'ASSOCIATION MONACO USA'
|
||||
});
|
||||
|
||||
// Reactive data - Using individual refs to prevent Vue reactivity corruption
|
||||
|
|
@ -258,6 +271,7 @@ const loading = ref(false);
|
|||
const recaptchaToken = ref('');
|
||||
const successMessage = ref('');
|
||||
const errorMessage = ref('');
|
||||
const configsLoaded = ref(false);
|
||||
|
||||
// Success dialog state
|
||||
const showSuccessDialog = ref(false);
|
||||
|
|
@ -268,10 +282,23 @@ useHead({
|
|||
title: 'Register - MonacoUSA Portal',
|
||||
meta: [
|
||||
{ name: 'description', content: 'Register to become a member of MonacoUSA Association' },
|
||||
{ name: 'robots', content: 'noindex, nofollow' }
|
||||
{ name: 'robots', content: 'noindex, nofollow' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' }
|
||||
]
|
||||
});
|
||||
|
||||
// Dynamic CSS classes based on device
|
||||
const containerClasses = computed(() => [
|
||||
'signup-container',
|
||||
...getOptimizedClasses()
|
||||
].join(' '));
|
||||
|
||||
const cardClasses = computed(() => [
|
||||
'signup-card',
|
||||
performanceMode.value ? 'performance-optimized' : '',
|
||||
disableBackdropFilter.value ? 'no-backdrop-filter' : ''
|
||||
].filter(Boolean).join(' '));
|
||||
|
||||
// Form validation rules
|
||||
const nameRules = [
|
||||
(v: string) => !!v || 'Name is required',
|
||||
|
|
@ -465,9 +492,10 @@ onMounted(async () => {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Base container styles */
|
||||
.signup-container {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh; /* Dynamic viewport height for mobile */
|
||||
min-height: calc(var(--vh, 1vh) * 100); /* Mobile Safari fallback */
|
||||
background: linear-gradient(rgba(163, 21, 21, 0.7), rgba(0, 0, 0, 0.5)),
|
||||
url('/monaco_high_res.jpg');
|
||||
background-size: cover;
|
||||
|
|
@ -476,6 +504,19 @@ onMounted(async () => {
|
|||
background-attachment: scroll;
|
||||
padding: 20px 0;
|
||||
position: relative;
|
||||
overflow-x: hidden; /* Prevent horizontal scroll on mobile */
|
||||
}
|
||||
|
||||
/* Mobile Safari optimizations */
|
||||
.signup-container.is-mobile-safari {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
background-attachment: scroll !important; /* Force scroll attachment */
|
||||
}
|
||||
|
||||
.signup-container.performance-mode {
|
||||
will-change: auto; /* Reduce repainting */
|
||||
transform: translateZ(0); /* Force hardware acceleration but lighter */
|
||||
}
|
||||
|
||||
/* Ensure background covers full content */
|
||||
|
|
@ -496,6 +537,13 @@ onMounted(async () => {
|
|||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* Performance mode background - simpler for mobile */
|
||||
.signup-container.performance-mode::before {
|
||||
background: linear-gradient(rgba(163, 21, 21, 0.8), rgba(0, 0, 0, 0.6));
|
||||
/* Remove background image on low-performance devices */
|
||||
}
|
||||
|
||||
/* Default card styles */
|
||||
.signup-card {
|
||||
backdrop-filter: blur(15px);
|
||||
background: rgba(255, 255, 255, 0.95) !important;
|
||||
|
|
@ -507,10 +555,30 @@ onMounted(async () => {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
/* Performance optimized card */
|
||||
.signup-card.performance-optimized {
|
||||
backdrop-filter: none; /* Remove expensive filter */
|
||||
background: rgba(255, 255, 255, 0.98) !important;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2) !important; /* Lighter shadow */
|
||||
transition: none; /* Remove animations for better performance */
|
||||
}
|
||||
|
||||
/* No backdrop filter fallback */
|
||||
.signup-card.no-backdrop-filter {
|
||||
backdrop-filter: none;
|
||||
background: rgba(255, 255, 255, 0.98) !important;
|
||||
}
|
||||
|
||||
.signup-card:hover {
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4) !important;
|
||||
}
|
||||
|
||||
/* Disable hover effects on performance mode */
|
||||
.signup-card.performance-optimized:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2) !important;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.payment-info {
|
||||
background: rgba(163, 21, 21, 0.05);
|
||||
border-radius: 12px;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Mobile Safari Fixes Plugin
|
||||
* Applies mobile Safari specific optimizations on client side
|
||||
*/
|
||||
|
||||
import { applyMobileSafariFixes } from '~/utils/mobile-safari-utils';
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
// Apply mobile Safari fixes on client-side mount
|
||||
if (typeof window !== 'undefined') {
|
||||
// Apply fixes immediately
|
||||
applyMobileSafariFixes();
|
||||
|
||||
// Also apply on route changes (for SPA navigation)
|
||||
const router = useRouter();
|
||||
router.afterEach(() => {
|
||||
// Small delay to ensure DOM is ready
|
||||
nextTick(() => {
|
||||
applyMobileSafariFixes();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
/**
|
||||
* Mobile Safari Detection and Optimization Utilities
|
||||
* Handles Safari-specific issues and performance optimizations
|
||||
*/
|
||||
|
||||
export interface DeviceInfo {
|
||||
isMobile: boolean;
|
||||
isSafari: boolean;
|
||||
isMobileSafari: boolean;
|
||||
isIOS: boolean;
|
||||
safariVersion?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect device and browser information
|
||||
*/
|
||||
export function getDeviceInfo(): DeviceInfo {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
isMobile: false,
|
||||
isSafari: false,
|
||||
isMobileSafari: false,
|
||||
isIOS: false
|
||||
};
|
||||
}
|
||||
|
||||
const userAgent = navigator.userAgent;
|
||||
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
||||
const isIOS = /iPad|iPhone|iPod/.test(userAgent);
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
|
||||
const isMobileSafari = isIOS && isSafari;
|
||||
|
||||
// Extract Safari version if possible
|
||||
let safariVersion: number | undefined;
|
||||
if (isSafari) {
|
||||
const match = userAgent.match(/Version\/(\d+)/);
|
||||
if (match) {
|
||||
safariVersion = parseInt(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
isSafari,
|
||||
isMobileSafari,
|
||||
isIOS,
|
||||
safariVersion
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the device needs performance optimizations
|
||||
*/
|
||||
export function needsPerformanceOptimization(): boolean {
|
||||
const { isMobileSafari, isMobile } = getDeviceInfo();
|
||||
return isMobileSafari || isMobile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if backdrop-filter should be disabled
|
||||
*/
|
||||
export function shouldDisableBackdropFilter(): boolean {
|
||||
const { isMobileSafari, safariVersion } = getDeviceInfo();
|
||||
// Disable backdrop-filter on mobile Safari or older Safari versions
|
||||
return isMobileSafari || Boolean(safariVersion && safariVersion < 16);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get optimized CSS class names based on device
|
||||
*/
|
||||
export function getOptimizedClasses(): string[] {
|
||||
const classes: string[] = [];
|
||||
const { isMobile, isMobileSafari, isIOS } = getDeviceInfo();
|
||||
|
||||
if (isMobile) classes.push('is-mobile');
|
||||
if (isMobileSafari) classes.push('is-mobile-safari');
|
||||
if (isIOS) classes.push('is-ios');
|
||||
if (needsPerformanceOptimization()) classes.push('performance-mode');
|
||||
|
||||
return classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimized viewport height for mobile Safari
|
||||
*/
|
||||
export function getOptimizedViewportHeight(): string {
|
||||
const { isMobileSafari, safariVersion } = getDeviceInfo();
|
||||
|
||||
if (isMobileSafari) {
|
||||
// Use 100vh for older Safari, -webkit-fill-available for newer
|
||||
return safariVersion && safariVersion >= 15 ? '-webkit-fill-available' : '100vh';
|
||||
}
|
||||
|
||||
// Use dvh for modern browsers, vh as fallback
|
||||
return '100vh';
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply mobile Safari specific fixes
|
||||
*/
|
||||
export function applyMobileSafariFixes(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const { isMobileSafari } = getDeviceInfo();
|
||||
if (!isMobileSafari) return;
|
||||
|
||||
// Fix viewport height issues
|
||||
const setViewportHeight = () => {
|
||||
const vh = window.innerHeight * 0.01;
|
||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||
};
|
||||
|
||||
// Set initial value
|
||||
setViewportHeight();
|
||||
|
||||
// Update on resize (debounced)
|
||||
let resizeTimeout: NodeJS.Timeout;
|
||||
window.addEventListener('resize', () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = setTimeout(setViewportHeight, 100);
|
||||
});
|
||||
|
||||
// Add performance optimization classes
|
||||
document.documentElement.classList.add(...getOptimizedClasses());
|
||||
}
|
||||
|
||||
/**
|
||||
* Throttle function for performance
|
||||
*/
|
||||
export function throttle<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
let previous = 0;
|
||||
|
||||
return function(this: any, ...args: Parameters<T>) {
|
||||
const now = Date.now();
|
||||
const remaining = wait - (now - previous);
|
||||
|
||||
if (remaining <= 0 || remaining > wait) {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
timeout = null;
|
||||
}
|
||||
previous = now;
|
||||
func.apply(this, args);
|
||||
} else if (!timeout) {
|
||||
timeout = setTimeout(() => {
|
||||
previous = Date.now();
|
||||
timeout = null;
|
||||
func.apply(this, args);
|
||||
}, remaining);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Debounce function for performance
|
||||
*/
|
||||
export function debounce<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
wait: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timeout: NodeJS.Timeout;
|
||||
|
||||
return function(this: any, ...args: Parameters<T>) {
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue