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>
|
<template>
|
||||||
<div class="signup-container">
|
<div :class="containerClasses">
|
||||||
<v-container fluid class="fill-height">
|
<v-container fluid class="fill-height">
|
||||||
<v-row class="fill-height" justify="center" align="center">
|
<v-row class="fill-height" justify="center" align="center">
|
||||||
<v-col cols="12" sm="10" md="8" lg="6" xl="5">
|
<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">
|
<v-card-text class="pa-8">
|
||||||
<!-- Logo and Welcome -->
|
<!-- Logo and Welcome -->
|
||||||
<div class="text-center mb-6">
|
<div class="text-center mb-6">
|
||||||
|
|
@ -209,6 +209,14 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { RegistrationFormData, RecaptchaConfig, RegistrationConfig } from '~/utils/types';
|
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 grecaptcha interface for TypeScript
|
||||||
declare global {
|
declare global {
|
||||||
|
|
@ -225,12 +233,17 @@ definePageMeta({
|
||||||
layout: false
|
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 recaptchaConfig = ref<RecaptchaConfig>({ siteKey: '', secretKey: '' });
|
||||||
const registrationConfig = ref<RegistrationConfig>({
|
const registrationConfig = ref<RegistrationConfig>({
|
||||||
membershipFee: 50,
|
membershipFee: 150,
|
||||||
iban: '',
|
iban: 'MC58 1756 9000 0104 0050 1001 860',
|
||||||
accountHolder: ''
|
accountHolder: 'ASSOCIATION MONACO USA'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reactive data - Using individual refs to prevent Vue reactivity corruption
|
// Reactive data - Using individual refs to prevent Vue reactivity corruption
|
||||||
|
|
@ -258,6 +271,7 @@ const loading = ref(false);
|
||||||
const recaptchaToken = ref('');
|
const recaptchaToken = ref('');
|
||||||
const successMessage = ref('');
|
const successMessage = ref('');
|
||||||
const errorMessage = ref('');
|
const errorMessage = ref('');
|
||||||
|
const configsLoaded = ref(false);
|
||||||
|
|
||||||
// Success dialog state
|
// Success dialog state
|
||||||
const showSuccessDialog = ref(false);
|
const showSuccessDialog = ref(false);
|
||||||
|
|
@ -268,10 +282,23 @@ useHead({
|
||||||
title: 'Register - MonacoUSA Portal',
|
title: 'Register - MonacoUSA Portal',
|
||||||
meta: [
|
meta: [
|
||||||
{ name: 'description', content: 'Register to become a member of MonacoUSA Association' },
|
{ 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
|
// Form validation rules
|
||||||
const nameRules = [
|
const nameRules = [
|
||||||
(v: string) => !!v || 'Name is required',
|
(v: string) => !!v || 'Name is required',
|
||||||
|
|
@ -465,9 +492,10 @@ onMounted(async () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
/* Base container styles */
|
||||||
.signup-container {
|
.signup-container {
|
||||||
min-height: 100vh;
|
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)),
|
background: linear-gradient(rgba(163, 21, 21, 0.7), rgba(0, 0, 0, 0.5)),
|
||||||
url('/monaco_high_res.jpg');
|
url('/monaco_high_res.jpg');
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
|
@ -476,6 +504,19 @@ onMounted(async () => {
|
||||||
background-attachment: scroll;
|
background-attachment: scroll;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
position: relative;
|
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 */
|
/* Ensure background covers full content */
|
||||||
|
|
@ -496,6 +537,13 @@ onMounted(async () => {
|
||||||
min-height: 100%;
|
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 {
|
.signup-card {
|
||||||
backdrop-filter: blur(15px);
|
backdrop-filter: blur(15px);
|
||||||
background: rgba(255, 255, 255, 0.95) !important;
|
background: rgba(255, 255, 255, 0.95) !important;
|
||||||
|
|
@ -507,10 +555,30 @@ onMounted(async () => {
|
||||||
width: 100%;
|
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 {
|
.signup-card:hover {
|
||||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4) !important;
|
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 {
|
.payment-info {
|
||||||
background: rgba(163, 21, 21, 0.05);
|
background: rgba(163, 21, 21, 0.05);
|
||||||
border-radius: 12px;
|
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