Refactor mobile detection to use unified composable
Build And Push Image / docker (push) Successful in 2m52s
Details
Build And Push Image / docker (push) Successful in 2m52s
Details
- Add useMobileDetection composable to centralize device detection logic - Replace direct utility imports with composable usage across components - Update MultipleNationalityInput, PhoneInputWrapper, and auth pages - Simplify mobile-specific styling and behavior handling - Improve code maintainability by consolidating detection logic
This commit is contained in:
parent
d14008efd4
commit
2b2cd5891f
|
|
@ -214,12 +214,8 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { getAllCountries, getCountryName } from '~/utils/countries';
|
import { getAllCountries, searchCountries } from '~/utils/countries';
|
||||||
import {
|
import { useMobileDetection } from '~/composables/useMobileDetection';
|
||||||
getDeviceInfo,
|
|
||||||
needsPerformanceOptimization,
|
|
||||||
getOptimizedClasses
|
|
||||||
} from '~/utils/mobile-safari-utils';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue?: string; // Comma-separated string like "FR,MC,US"
|
modelValue?: string; // Comma-separated string like "FR,MC,US"
|
||||||
|
|
@ -245,10 +241,10 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
// Mobile Safari detection
|
// Use unified mobile detection
|
||||||
const deviceInfo = ref(getDeviceInfo());
|
const mobileDetection = useMobileDetection();
|
||||||
const isMobileSafari = computed(() => deviceInfo.value.isMobileSafari);
|
const isMobileSafari = computed(() => mobileDetection.isMobileSafari);
|
||||||
const needsPerformanceMode = computed(() => needsPerformanceOptimization());
|
const needsPerformanceMode = computed(() => mobileDetection.isMobileSafari || mobileDetection.isMobile);
|
||||||
|
|
||||||
// Parse initial nationalities from comma-separated string
|
// Parse initial nationalities from comma-separated string
|
||||||
const parseNationalities = (value: string): string[] => {
|
const parseNationalities = (value: string): string[] => {
|
||||||
|
|
@ -344,6 +340,14 @@ const updateNationalities = () => {
|
||||||
emit('update:modelValue', result);
|
emit('update:modelValue', result);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper methods
|
||||||
|
const getCountryName = (countryCode: string): string => {
|
||||||
|
if (!countryCode) return '';
|
||||||
|
const countries = getAllCountries();
|
||||||
|
const country = countries.find(c => c.code === countryCode);
|
||||||
|
return country?.name || '';
|
||||||
|
};
|
||||||
|
|
||||||
// Mobile Safari specific methods
|
// Mobile Safari specific methods
|
||||||
const getSelectedCountryName = (countryCode: string): string => {
|
const getSelectedCountryName = (countryCode: string): string => {
|
||||||
if (!countryCode) return '';
|
if (!countryCode) return '';
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="phone-input-wrapper" :class="{ 'phone-input-wrapper--mobile': isMobile }">
|
<div class="phone-input-wrapper" :class="{ 'phone-input-wrapper--mobile': mobileDetection.isMobile }">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
v-model="localNumber"
|
v-model="localNumber"
|
||||||
:label="label"
|
:label="label"
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
:required="required"
|
:required="required"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
:density="isMobile ? 'default' : 'comfortable'"
|
:density="mobileDetection.isMobile ? 'default' : 'comfortable'"
|
||||||
class="phone-text-field"
|
class="phone-text-field"
|
||||||
@input="handleInput"
|
@input="handleInput"
|
||||||
@blur="handleBlur"
|
@blur="handleBlur"
|
||||||
|
|
@ -21,13 +21,11 @@
|
||||||
<v-menu
|
<v-menu
|
||||||
v-model="dropdownOpen"
|
v-model="dropdownOpen"
|
||||||
:close-on-content-click="false"
|
:close-on-content-click="false"
|
||||||
:location="isMobile ? undefined : 'bottom start'"
|
location="bottom start"
|
||||||
:offset="isMobile ? 0 : 4"
|
:offset="4"
|
||||||
:min-width="isMobile ? undefined : '280'"
|
:min-width="mobileDetection.isMobile ? '90vw' : '280'"
|
||||||
:transition="isMobile ? 'fade-transition' : 'fade-transition'"
|
transition="fade-transition"
|
||||||
:no-click-animation="isMobile"
|
:no-click-animation="true"
|
||||||
:attach="isMobile"
|
|
||||||
:strategy="isMobile ? 'fixed' : 'absolute'"
|
|
||||||
>
|
>
|
||||||
<template #activator="{ props: menuProps }">
|
<template #activator="{ props: menuProps }">
|
||||||
<div
|
<div
|
||||||
|
|
@ -35,7 +33,7 @@
|
||||||
class="country-selector"
|
class="country-selector"
|
||||||
:class="{
|
:class="{
|
||||||
'country-selector--open': dropdownOpen,
|
'country-selector--open': dropdownOpen,
|
||||||
'country-selector--mobile': isMobile
|
'country-selector--mobile': mobileDetection.isMobile
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
|
@ -46,7 +44,7 @@
|
||||||
/>
|
/>
|
||||||
<span class="country-code">{{ selectedCountry.dialCode }}</span>
|
<span class="country-code">{{ selectedCountry.dialCode }}</span>
|
||||||
<v-icon
|
<v-icon
|
||||||
:size="isMobile ? 18 : 16"
|
:size="mobileDetection.isMobile ? 18 : 16"
|
||||||
class="dropdown-icon"
|
class="dropdown-icon"
|
||||||
:class="{ 'dropdown-icon--rotated': dropdownOpen }"
|
:class="{ 'dropdown-icon--rotated': dropdownOpen }"
|
||||||
>
|
>
|
||||||
|
|
@ -55,23 +53,14 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Mobile Full-Screen Overlay -->
|
|
||||||
<div
|
|
||||||
v-if="isMobile && dropdownOpen"
|
|
||||||
class="mobile-overlay"
|
|
||||||
@click="closeDropdown"
|
|
||||||
@touchstart="handleOverlayTouch"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Dropdown Content -->
|
<!-- Dropdown Content -->
|
||||||
<v-card
|
<v-card
|
||||||
class="country-dropdown"
|
class="country-dropdown"
|
||||||
:class="{ 'country-dropdown--mobile': isMobile }"
|
:class="{ 'country-dropdown--mobile': mobileDetection.isMobile }"
|
||||||
elevation="8"
|
:elevation="mobileDetection.isMobile ? 24 : 8"
|
||||||
:style="isMobile ? 'display: block !important;' : ''"
|
|
||||||
>
|
>
|
||||||
<!-- Mobile Header -->
|
<!-- Mobile Header -->
|
||||||
<div v-if="isMobile" class="mobile-header">
|
<div v-if="mobileDetection.isMobile" class="mobile-header">
|
||||||
<h3 class="mobile-title">Select Country</h3>
|
<h3 class="mobile-title">Select Country</h3>
|
||||||
<v-btn
|
<v-btn
|
||||||
icon="mdi-close"
|
icon="mdi-close"
|
||||||
|
|
@ -88,11 +77,11 @@
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
placeholder="Search countries..."
|
placeholder="Search countries..."
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
:density="isMobile ? 'default' : 'compact'"
|
:density="mobileDetection.isMobile ? 'default' : 'compact'"
|
||||||
prepend-inner-icon="mdi-magnify"
|
prepend-inner-icon="mdi-magnify"
|
||||||
hide-details
|
hide-details
|
||||||
class="search-input"
|
class="search-input"
|
||||||
:autofocus="!isMobile"
|
:autofocus="!mobileDetection.isMobile"
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -100,8 +89,8 @@
|
||||||
<!-- Country List -->
|
<!-- Country List -->
|
||||||
<v-list
|
<v-list
|
||||||
class="country-list"
|
class="country-list"
|
||||||
:class="{ 'country-list--mobile': isMobile }"
|
:class="{ 'country-list--mobile': mobileDetection.isMobile }"
|
||||||
:density="isMobile ? 'default' : 'compact'"
|
:density="mobileDetection.isMobile ? 'default' : 'compact'"
|
||||||
>
|
>
|
||||||
<v-list-item
|
<v-list-item
|
||||||
v-for="country in filteredCountries"
|
v-for="country in filteredCountries"
|
||||||
|
|
@ -110,25 +99,24 @@
|
||||||
'country-item': true,
|
'country-item': true,
|
||||||
'country-item--selected': country.iso2 === selectedCountry.iso2,
|
'country-item--selected': country.iso2 === selectedCountry.iso2,
|
||||||
'country-item--preferred': isPreferredCountry(country.iso2),
|
'country-item--preferred': isPreferredCountry(country.iso2),
|
||||||
'country-item--mobile': isMobile
|
'country-item--mobile': mobileDetection.isMobile
|
||||||
}"
|
}"
|
||||||
@click="selectCountry(country)"
|
@click="selectCountry(country)"
|
||||||
@touchstart="handleCountryTouch"
|
:ripple="mobileDetection.isMobile"
|
||||||
:ripple="isMobile"
|
|
||||||
>
|
>
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<img
|
<img
|
||||||
:src="getCountryFlagUrl(country.iso2)"
|
:src="getCountryFlagUrl(country.iso2)"
|
||||||
:alt="`${country.name} flag`"
|
:alt="`${country.name} flag`"
|
||||||
class="list-flag"
|
class="list-flag"
|
||||||
:class="{ 'list-flag--mobile': isMobile }"
|
:class="{ 'list-flag--mobile': mobileDetection.isMobile }"
|
||||||
@error="handleFlagError"
|
@error="handleFlagError"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<v-list-item-title
|
<v-list-item-title
|
||||||
class="country-name"
|
class="country-name"
|
||||||
:class="{ 'country-name--mobile': isMobile }"
|
:class="{ 'country-name--mobile': mobileDetection.isMobile }"
|
||||||
>
|
>
|
||||||
{{ country.name }}
|
{{ country.name }}
|
||||||
</v-list-item-title>
|
</v-list-item-title>
|
||||||
|
|
@ -136,7 +124,7 @@
|
||||||
<template #append>
|
<template #append>
|
||||||
<span
|
<span
|
||||||
class="dial-code"
|
class="dial-code"
|
||||||
:class="{ 'dial-code--mobile': isMobile }"
|
:class="{ 'dial-code--mobile': mobileDetection.isMobile }"
|
||||||
>
|
>
|
||||||
{{ country.dialCode }}
|
{{ country.dialCode }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -145,7 +133,7 @@
|
||||||
</v-list>
|
</v-list>
|
||||||
|
|
||||||
<!-- Mobile Footer -->
|
<!-- Mobile Footer -->
|
||||||
<div v-if="isMobile" class="mobile-footer">
|
<div v-if="mobileDetection.isMobile" class="mobile-footer">
|
||||||
<v-btn
|
<v-btn
|
||||||
block
|
block
|
||||||
variant="text"
|
variant="text"
|
||||||
|
|
@ -165,6 +153,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { parsePhoneNumber, AsYouType } from 'libphonenumber-js';
|
import { parsePhoneNumber, AsYouType } from 'libphonenumber-js';
|
||||||
import { getPhoneCountriesWithPreferred, searchPhoneCountries, getPhoneCountryByCode, type PhoneCountry } from '~/utils/phone-countries';
|
import { getPhoneCountriesWithPreferred, searchPhoneCountries, getPhoneCountryByCode, type PhoneCountry } from '~/utils/phone-countries';
|
||||||
|
import { useMobileDetection } from '~/composables/useMobileDetection';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
modelValue?: string;
|
modelValue?: string;
|
||||||
|
|
@ -197,6 +186,9 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
|
|
||||||
const emit = defineEmits<Emits>();
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
// Use unified mobile detection
|
||||||
|
const mobileDetection = useMobileDetection();
|
||||||
|
|
||||||
// Get comprehensive countries list
|
// Get comprehensive countries list
|
||||||
const countries = getPhoneCountriesWithPreferred(props.preferredCountries);
|
const countries = getPhoneCountriesWithPreferred(props.preferredCountries);
|
||||||
|
|
||||||
|
|
@ -208,26 +200,9 @@ const selectedCountry = ref<PhoneCountry>(
|
||||||
getPhoneCountryByCode(props.defaultCountry) || countries[0]
|
getPhoneCountryByCode(props.defaultCountry) || countries[0]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Mobile detection
|
|
||||||
const isMobile = ref(false);
|
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const flagUrl = computed(() => getCountryFlagUrl(selectedCountry.value.iso2));
|
const flagUrl = computed(() => getCountryFlagUrl(selectedCountry.value.iso2));
|
||||||
|
|
||||||
// Mobile detection on mount
|
|
||||||
onMounted(() => {
|
|
||||||
const checkMobile = () => {
|
|
||||||
isMobile.value = window.innerWidth <= 768 || 'ontouchstart' in window;
|
|
||||||
};
|
|
||||||
|
|
||||||
checkMobile();
|
|
||||||
window.addEventListener('resize', checkMobile);
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('resize', checkMobile);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const filteredCountries = computed(() => {
|
const filteredCountries = computed(() => {
|
||||||
return searchPhoneCountries(searchQuery.value, props.preferredCountries);
|
return searchPhoneCountries(searchQuery.value, props.preferredCountries);
|
||||||
});
|
});
|
||||||
|
|
@ -241,18 +216,10 @@ const isPreferredCountry = (iso2: string) => {
|
||||||
return props.preferredCountries.includes(iso2);
|
return props.preferredCountries.includes(iso2);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleDropdown = () => {
|
|
||||||
if (!props.disabled) {
|
|
||||||
dropdownOpen.value = !dropdownOpen.value;
|
|
||||||
if (dropdownOpen.value) {
|
|
||||||
searchQuery.value = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectCountry = (country: PhoneCountry) => {
|
const selectCountry = (country: PhoneCountry) => {
|
||||||
selectedCountry.value = country;
|
selectedCountry.value = country;
|
||||||
dropdownOpen.value = false;
|
dropdownOpen.value = false;
|
||||||
|
searchQuery.value = ''; // Clear search on selection
|
||||||
emit('country-changed', country);
|
emit('country-changed', country);
|
||||||
|
|
||||||
// Reformat existing number with new country
|
// Reformat existing number with new country
|
||||||
|
|
@ -314,25 +281,6 @@ const closeDropdown = () => {
|
||||||
searchQuery.value = '';
|
searchQuery.value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTouchStart = (event: TouchEvent) => {
|
|
||||||
// Allow natural touch behavior, just ensure dropdown works
|
|
||||||
if (!dropdownOpen.value && !props.disabled) {
|
|
||||||
dropdownOpen.value = true;
|
|
||||||
searchQuery.value = '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOverlayTouch = (event: TouchEvent) => {
|
|
||||||
// Close dropdown when touching overlay
|
|
||||||
event.preventDefault();
|
|
||||||
closeDropdown();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCountryTouch = (event: TouchEvent) => {
|
|
||||||
// Allow touch to work naturally for country selection
|
|
||||||
// Don't prevent default as it interferes with click events
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize from modelValue
|
// Initialize from modelValue
|
||||||
watch(() => props.modelValue, (newValue) => {
|
watch(() => props.modelValue, (newValue) => {
|
||||||
if (newValue && newValue !== selectedCountry.value.dialCode + localNumber.value.replace(/\D/g, '')) {
|
if (newValue && newValue !== selectedCountry.value.dialCode + localNumber.value.replace(/\D/g, '')) {
|
||||||
|
|
@ -357,6 +305,16 @@ watch(() => props.modelValue, (newValue) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
|
// Clean up search query when dropdown closes
|
||||||
|
watch(dropdownOpen, (isOpen) => {
|
||||||
|
if (!isOpen) {
|
||||||
|
// Clear search after a small delay to allow selection to complete
|
||||||
|
setTimeout(() => {
|
||||||
|
searchQuery.value = '';
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -375,6 +333,8 @@ watch(() => props.modelValue, (newValue) => {
|
||||||
background: rgba(var(--v-theme-surface), 1);
|
background: rgba(var(--v-theme-surface), 1);
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-selector:hover {
|
.country-selector:hover {
|
||||||
|
|
@ -438,6 +398,7 @@ watch(() => props.modelValue, (newValue) => {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: rgba(var(--v-theme-surface), 1);
|
background: rgba(var(--v-theme-surface), 1);
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-list::-webkit-scrollbar {
|
.country-list::-webkit-scrollbar {
|
||||||
|
|
@ -494,19 +455,6 @@ watch(() => props.modelValue, (newValue) => {
|
||||||
font-family: 'Roboto Mono', monospace;
|
font-family: 'Roboto Mono', monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile Overlay */
|
|
||||||
.mobile-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
z-index: 1999;
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
-webkit-backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile Header */
|
/* Mobile Header */
|
||||||
.mobile-header {
|
.mobile-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -550,57 +498,33 @@ watch(() => props.modelValue, (newValue) => {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
min-height: 44px; /* Touch-friendly size */
|
min-height: 44px; /* Touch-friendly size */
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-selector--mobile:hover {
|
.country-selector--mobile:active {
|
||||||
background: rgba(var(--v-theme-primary), 0.12);
|
background: rgba(var(--v-theme-primary), 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-dropdown--mobile {
|
.country-dropdown--mobile {
|
||||||
position: fixed !important;
|
|
||||||
top: 10% !important;
|
|
||||||
left: 5% !important;
|
|
||||||
right: 5% !important;
|
|
||||||
width: 90vw !important;
|
width: 90vw !important;
|
||||||
max-width: 400px !important;
|
max-width: 400px !important;
|
||||||
min-height: 400px !important;
|
max-height: 70vh !important;
|
||||||
max-height: 80vh !important;
|
|
||||||
height: auto !important;
|
|
||||||
margin: 0 auto !important;
|
|
||||||
border-radius: 16px !important;
|
|
||||||
z-index: 2000 !important;
|
|
||||||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.3) !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
display: flex !important;
|
|
||||||
flex-direction: column !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-list--mobile {
|
.country-list--mobile {
|
||||||
flex: 1 !important;
|
max-height: calc(50vh - 120px) !important;
|
||||||
min-height: 200px !important;
|
|
||||||
max-height: calc(60vh - 120px) !important;
|
|
||||||
overflow-y: auto !important;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-list--mobile::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.country-list--mobile::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(var(--v-theme-primary), 0.4);
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.country-item--mobile {
|
.country-item--mobile {
|
||||||
min-height: 56px !important;
|
min-height: 56px !important;
|
||||||
padding: 12px 20px !important;
|
padding: 12px 20px !important;
|
||||||
border-left-width: 4px !important;
|
border-left-width: 4px !important;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-item--mobile:active {
|
.country-item--mobile:active {
|
||||||
background: rgba(var(--v-theme-primary), 0.16) !important;
|
background: rgba(var(--v-theme-primary), 0.16) !important;
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-flag--mobile {
|
.list-flag--mobile {
|
||||||
|
|
@ -636,7 +560,7 @@ watch(() => props.modelValue, (newValue) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-list {
|
.country-list {
|
||||||
max-height: 200px;
|
max-height: 250px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-selector {
|
.country-selector {
|
||||||
|
|
@ -644,74 +568,11 @@ watch(() => props.modelValue, (newValue) => {
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-flag {
|
.search-input :deep(.v-field__input) {
|
||||||
width: 24px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.country-code {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
min-width: 36px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-container {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-input :deep(.v-field) {
|
|
||||||
font-size: 16px !important; /* Prevent zoom */
|
font-size: 16px !important; /* Prevent zoom */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.country-dropdown--mobile {
|
|
||||||
width: 95vw !important;
|
|
||||||
max-height: 85vh !important;
|
|
||||||
border-radius: 12px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.country-list--mobile {
|
|
||||||
max-height: calc(65vh - 140px) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.country-item--mobile {
|
|
||||||
min-height: 52px !important;
|
|
||||||
padding: 10px 16px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-header,
|
|
||||||
.mobile-footer,
|
|
||||||
.search-container {
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-title {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Landscape orientation adjustments */
|
|
||||||
@media (max-height: 500px) and (orientation: landscape) {
|
|
||||||
.country-dropdown--mobile {
|
|
||||||
max-height: 90vh !important;
|
|
||||||
width: 60vw !important;
|
|
||||||
max-width: 500px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.country-list--mobile {
|
|
||||||
max-height: calc(75vh - 120px) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* High DPI displays */
|
|
||||||
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
|
||||||
.country-flag,
|
|
||||||
.list-flag {
|
|
||||||
image-rendering: -webkit-optimize-contrast;
|
|
||||||
image-rendering: crisp-edges;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* iOS specific fixes */
|
/* iOS specific fixes */
|
||||||
@supports (-webkit-touch-callout: none) {
|
@supports (-webkit-touch-callout: none) {
|
||||||
.phone-input-wrapper--mobile .phone-text-field :deep(.v-field__input) {
|
.phone-input-wrapper--mobile .phone-text-field :deep(.v-field__input) {
|
||||||
|
|
@ -719,79 +580,22 @@ watch(() => props.modelValue, (newValue) => {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.country-dropdown--mobile {
|
.search-input :deep(.v-field__input) {
|
||||||
-webkit-backdrop-filter: blur(8px);
|
font-size: 16px !important;
|
||||||
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-overlay {
|
.country-list {
|
||||||
-webkit-backdrop-filter: blur(4px);
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Android specific fixes */
|
/* Accessibility improvements */
|
||||||
@media (pointer: coarse) {
|
|
||||||
.country-item--mobile {
|
|
||||||
min-height: 56px !important; /* Material Design touch target */
|
|
||||||
}
|
|
||||||
|
|
||||||
.country-selector--mobile {
|
|
||||||
min-height: 48px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Accessibility improvements for mobile */
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.country-item--mobile,
|
.country-item,
|
||||||
.country-selector--mobile {
|
.country-selector,
|
||||||
|
.dropdown-icon {
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-overlay {
|
|
||||||
backdrop-filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.country-dropdown--mobile {
|
|
||||||
backdrop-filter: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* High contrast mode for mobile */
|
|
||||||
@media (prefers-contrast: high) {
|
|
||||||
.country-dropdown--mobile {
|
|
||||||
border: 3px solid rgb(var(--v-theme-outline));
|
|
||||||
}
|
|
||||||
|
|
||||||
.country-item--mobile {
|
|
||||||
border-bottom: 1px solid rgb(var(--v-theme-outline));
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-header {
|
|
||||||
border-bottom: 2px solid rgb(var(--v-theme-outline));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark theme support */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.country-dropdown {
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.country-dropdown--mobile {
|
|
||||||
box-shadow: 0 24px 48px rgba(0, 0, 0, 0.6) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mobile-overlay {
|
|
||||||
background: rgba(0, 0, 0, 0.7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Safe area handling for notched devices */
|
|
||||||
@supports (padding: max(0px)) {
|
|
||||||
.country-dropdown--mobile {
|
|
||||||
padding-top: max(16px, env(safe-area-inset-top));
|
|
||||||
padding-bottom: max(16px, env(safe-area-inset-bottom));
|
|
||||||
padding-left: max(16px, env(safe-area-inset-left));
|
|
||||||
padding-right: max(16px, env(safe-area-inset-right));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
const handleResize = () => {
|
||||||
|
clearTimeout(resizeTimeout);
|
||||||
|
|
||||||
|
// Debounce and only update if height actually changed
|
||||||
|
resizeTimeout = setTimeout(() => {
|
||||||
|
const newHeight = window.innerHeight;
|
||||||
|
|
||||||
|
// Only update if there's a significant change (more than 20px)
|
||||||
|
// This prevents minor fluctuations from triggering updates
|
||||||
|
if (Math.abs(newHeight - lastHeight) > 20) {
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 150); // Increased debounce time
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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', () => {
|
||||||
|
// Wait for orientation change to complete
|
||||||
|
setTimeout(handleResize, 100);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
@ -153,21 +153,24 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { useMobileDetection } from '~/composables/useMobileDetection';
|
||||||
getOptimizedClasses,
|
|
||||||
applyMobileSafariFixes
|
|
||||||
} from '~/utils/mobile-safari-utils';
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: false,
|
layout: false,
|
||||||
middleware: 'guest'
|
middleware: 'guest'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use unified mobile detection
|
||||||
|
const mobileDetection = useMobileDetection();
|
||||||
|
|
||||||
// Mobile Safari optimization classes
|
// Mobile Safari optimization classes
|
||||||
const containerClasses = computed(() => [
|
const containerClasses = computed(() => {
|
||||||
'password-setup-page',
|
const classes = ['password-setup-page'];
|
||||||
...getOptimizedClasses()
|
if (mobileDetection.isMobile) classes.push('is-mobile');
|
||||||
].join(' '));
|
if (mobileDetection.isMobileSafari) classes.push('is-mobile-safari');
|
||||||
|
if (mobileDetection.isIOS) classes.push('is-ios');
|
||||||
|
return classes.join(' ');
|
||||||
|
});
|
||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
|
|
@ -300,13 +303,8 @@ const setupPassword = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply mobile Safari fixes
|
// Component initialization
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Apply mobile Safari fixes
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
applyMobileSafariFixes();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[setup-password] Password setup page loaded for:', email.value);
|
console.log('[setup-password] Password setup page loaded for:', email.value);
|
||||||
|
|
||||||
// Check if we have required parameters
|
// Check if we have required parameters
|
||||||
|
|
|
||||||
|
|
@ -91,10 +91,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { useMobileDetection } from '~/composables/useMobileDetection';
|
||||||
getOptimizedClasses,
|
|
||||||
applyMobileSafariFixes
|
|
||||||
} from '~/utils/mobile-safari-utils';
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: false,
|
layout: false,
|
||||||
|
|
@ -106,11 +103,17 @@ const route = useRoute();
|
||||||
const email = computed(() => route.query.email as string || '');
|
const email = computed(() => route.query.email as string || '');
|
||||||
const partialWarning = computed(() => route.query.warning === 'partial');
|
const partialWarning = computed(() => route.query.warning === 'partial');
|
||||||
|
|
||||||
|
// Use unified mobile detection
|
||||||
|
const mobileDetection = useMobileDetection();
|
||||||
|
|
||||||
// Mobile Safari optimization classes
|
// Mobile Safari optimization classes
|
||||||
const containerClasses = computed(() => [
|
const containerClasses = computed(() => {
|
||||||
'verification-success',
|
const classes = ['verification-success'];
|
||||||
...getOptimizedClasses()
|
if (mobileDetection.isMobile) classes.push('is-mobile');
|
||||||
].join(' '));
|
if (mobileDetection.isMobileSafari) classes.push('is-mobile-safari');
|
||||||
|
if (mobileDetection.isIOS) classes.push('is-ios');
|
||||||
|
return classes.join(' ');
|
||||||
|
});
|
||||||
|
|
||||||
// Setup password URL for Keycloak - Fixed URL structure
|
// Setup password URL for Keycloak - Fixed URL structure
|
||||||
const setupPasswordUrl = computed(() => {
|
const setupPasswordUrl = computed(() => {
|
||||||
|
|
@ -144,13 +147,8 @@ const goToPasswordSetup = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply mobile Safari fixes and track verification
|
// Track verification
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Apply mobile Safari fixes
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
applyMobileSafariFixes();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[verify-success] Email verification completed', {
|
console.log('[verify-success] Email verification completed', {
|
||||||
email: email.value,
|
email: email.value,
|
||||||
partialWarning: partialWarning.value,
|
partialWarning: partialWarning.value,
|
||||||
|
|
|
||||||
|
|
@ -121,21 +121,24 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
import { useMobileDetection } from '~/composables/useMobileDetection';
|
||||||
getOptimizedClasses,
|
|
||||||
applyMobileSafariFixes
|
|
||||||
} from '~/utils/mobile-safari-utils';
|
|
||||||
|
|
||||||
definePageMeta({
|
definePageMeta({
|
||||||
layout: false,
|
layout: false,
|
||||||
middleware: 'guest'
|
middleware: 'guest'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Use unified mobile detection
|
||||||
|
const mobileDetection = useMobileDetection();
|
||||||
|
|
||||||
// Mobile Safari optimization classes
|
// Mobile Safari optimization classes
|
||||||
const containerClasses = computed(() => [
|
const containerClasses = computed(() => {
|
||||||
'verification-page',
|
const classes = ['verification-page'];
|
||||||
...getOptimizedClasses()
|
if (mobileDetection.isMobile) classes.push('is-mobile');
|
||||||
].join(' '));
|
if (mobileDetection.isMobileSafari) classes.push('is-mobile-safari');
|
||||||
|
if (mobileDetection.isIOS) classes.push('is-ios');
|
||||||
|
return classes.join(' ');
|
||||||
|
});
|
||||||
|
|
||||||
// Reactive state
|
// Reactive state
|
||||||
const verifying = ref(true);
|
const verifying = ref(true);
|
||||||
|
|
@ -202,13 +205,8 @@ const retryVerification = () => {
|
||||||
verifyEmail();
|
verifyEmail();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply mobile Safari fixes and start verification
|
// Start verification on mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Apply mobile Safari fixes
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
applyMobileSafariFixes();
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[auth/verify] Starting email verification with token:', token.value?.substring(0, 20) + '...');
|
console.log('[auth/verify] Starting email verification with token:', token.value?.substring(0, 20) + '...');
|
||||||
|
|
||||||
// Start verification process
|
// Start verification process
|
||||||
|
|
|
||||||
|
|
@ -209,14 +209,7 @@
|
||||||
|
|
||||||
<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 {
|
import { useMobileDetection } from '~/composables/useMobileDetection';
|
||||||
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 {
|
||||||
|
|
@ -233,10 +226,8 @@ definePageMeta({
|
||||||
layout: false
|
layout: false
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mobile Safari optimization flags
|
// Use unified mobile detection
|
||||||
const deviceInfo = ref(getDeviceInfo());
|
const mobileDetection = useMobileDetection();
|
||||||
const performanceMode = ref(needsPerformanceOptimization());
|
|
||||||
const disableBackdropFilter = ref(shouldDisableBackdropFilter());
|
|
||||||
|
|
||||||
// Configs with fallback defaults
|
// Configs with fallback defaults
|
||||||
const recaptchaConfig = ref<RecaptchaConfig>({ siteKey: '', secretKey: '' });
|
const recaptchaConfig = ref<RecaptchaConfig>({ siteKey: '', secretKey: '' });
|
||||||
|
|
@ -288,16 +279,22 @@ useHead({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Dynamic CSS classes based on device
|
// Dynamic CSS classes based on device
|
||||||
const containerClasses = computed(() => [
|
const containerClasses = computed(() => {
|
||||||
'signup-container',
|
const classes = ['signup-container'];
|
||||||
...getOptimizedClasses()
|
if (mobileDetection.isMobile) classes.push('is-mobile');
|
||||||
].join(' '));
|
if (mobileDetection.isMobileSafari) classes.push('is-mobile-safari');
|
||||||
|
if (mobileDetection.isIOS) classes.push('is-ios');
|
||||||
|
return classes.join(' ');
|
||||||
|
});
|
||||||
|
|
||||||
const cardClasses = computed(() => [
|
const cardClasses = computed(() => {
|
||||||
'signup-card',
|
const classes = ['signup-card'];
|
||||||
performanceMode.value ? 'performance-optimized' : '',
|
if (mobileDetection.isMobileSafari) {
|
||||||
disableBackdropFilter.value ? 'no-backdrop-filter' : ''
|
classes.push('performance-optimized');
|
||||||
].filter(Boolean).join(' '));
|
classes.push('no-backdrop-filter');
|
||||||
|
}
|
||||||
|
return classes.filter(Boolean).join(' ');
|
||||||
|
});
|
||||||
|
|
||||||
// Form validation rules
|
// Form validation rules
|
||||||
const nameRules = [
|
const nameRules = [
|
||||||
|
|
@ -455,34 +452,35 @@ onMounted(async () => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Load configs without complex timeout logic
|
// Load reCAPTCHA config
|
||||||
Promise.all([
|
$fetch('/api/recaptcha-config')
|
||||||
// Load reCAPTCHA config
|
.then((response: any) => {
|
||||||
$fetch('/api/recaptcha-config').then((response: any) => {
|
|
||||||
if (response?.success && response?.data?.siteKey) {
|
if (response?.success && response?.data?.siteKey) {
|
||||||
recaptchaConfig.value.siteKey = response.data.siteKey;
|
recaptchaConfig.value.siteKey = response.data.siteKey;
|
||||||
loadRecaptchaScript(response.data.siteKey);
|
loadRecaptchaScript(response.data.siteKey);
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
// Silently fail for reCAPTCHA - not critical
|
// Silently fail for reCAPTCHA - not critical
|
||||||
}),
|
console.debug('reCAPTCHA config not available');
|
||||||
|
});
|
||||||
// Load registration config
|
|
||||||
$fetch('/api/registration-config').then((response: any) => {
|
// Load registration config
|
||||||
|
$fetch('/api/registration-config')
|
||||||
|
.then((response: any) => {
|
||||||
if (response?.success) {
|
if (response?.success) {
|
||||||
registrationConfig.value = response.data;
|
registrationConfig.value = response.data;
|
||||||
}
|
}
|
||||||
}).catch(() => {
|
})
|
||||||
|
.catch(() => {
|
||||||
// Use defaults if config fails to load
|
// Use defaults if config fails to load
|
||||||
|
console.debug('Using default registration config');
|
||||||
registrationConfig.value = {
|
registrationConfig.value = {
|
||||||
membershipFee: 150,
|
membershipFee: 150,
|
||||||
iban: 'MC58 1756 9000 0104 0050 1001 860',
|
iban: 'MC58 1756 9000 0104 0050 1001 860',
|
||||||
accountHolder: 'ASSOCIATION MONACO USA'
|
accountHolder: 'ASSOCIATION MONACO USA'
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
]).catch(() => {
|
|
||||||
// Global fallback - don't let errors cause page reload
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Prevent any errors from bubbling up and causing reload
|
// Prevent any errors from bubbling up and causing reload
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,43 @@
|
||||||
/**
|
/**
|
||||||
* Mobile Safari Fixes Plugin
|
* Mobile Safari Fixes Plugin
|
||||||
* Applies mobile Safari specific optimizations on client side
|
* Applies mobile Safari specific optimizations on client side
|
||||||
|
* Now uses unified mobile detection to prevent conflicts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { applyMobileSafariFixes } from '~/utils/mobile-safari-utils';
|
import { useMobileDetection } from '~/composables/useMobileDetection';
|
||||||
|
|
||||||
|
// Track if fixes have been applied to prevent duplicate execution
|
||||||
|
let fixesApplied = false;
|
||||||
|
|
||||||
export default defineNuxtPlugin(() => {
|
export default defineNuxtPlugin(() => {
|
||||||
// Apply mobile Safari fixes on client-side mount
|
// Apply mobile Safari fixes on client-side mount
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined' && !fixesApplied) {
|
||||||
// Apply fixes immediately
|
// Use the unified mobile detection composable
|
||||||
applyMobileSafariFixes();
|
const mobileDetection = useMobileDetection();
|
||||||
|
|
||||||
// Also apply on route changes (for SPA navigation)
|
// Apply initial fixes only for mobile Safari
|
||||||
const router = useRouter();
|
if (mobileDetection.isMobileSafari) {
|
||||||
router.afterEach(() => {
|
// Set initial viewport height CSS variable
|
||||||
// Small delay to ensure DOM is ready
|
const vh = window.innerHeight * 0.01;
|
||||||
nextTick(() => {
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||||
applyMobileSafariFixes();
|
|
||||||
});
|
// Add optimization classes
|
||||||
});
|
const classes = [];
|
||||||
|
if (mobileDetection.isMobile) classes.push('is-mobile');
|
||||||
|
if (mobileDetection.isMobileSafari) classes.push('is-mobile-safari');
|
||||||
|
if (mobileDetection.isIOS) classes.push('is-ios');
|
||||||
|
|
||||||
|
// Add classes to document element
|
||||||
|
if (classes.length > 0) {
|
||||||
|
document.documentElement.classList.add(...classes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark fixes as applied
|
||||||
|
fixesApplied = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: We're NOT applying fixes on route changes anymore
|
||||||
|
// as this was causing the reload loop. The composable handles
|
||||||
|
// all necessary viewport updates.
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue