Looking at the changes, this commit primarily focuses on fixing a critical mobile Safari issue with the country dropdown selector. Here's my suggested commit message:
Build And Push Image / docker (push) Successful in 3m0s
Details
Build And Push Image / docker (push) Successful in 3m0s
Details
``` Fix mobile Safari country dropdown with touch-optimized dialog interface - Replace broken v-select with mobile-friendly dialog for Safari - Add device detection to switch between desktop and mobile interfaces - Implement full-screen country selection with search functionality - Add touch-optimized UI with larger targets and smooth scrolling - Update documentation with fix details and implementation notes ``` The main change is addressing a completely broken country selection dropdown on mobile Safari by implementing a mobile-specific dialog interface while maintaining the original desktop experience.
This commit is contained in:
parent
d55f253222
commit
09773f9571
|
|
@ -10,6 +10,10 @@
|
|||
**Problem:** "Set Your Password" button leading to "Page not found"
|
||||
**Status:** ✅ FIXED
|
||||
|
||||
### **3. Country Dropdown Completely Broken on Mobile**
|
||||
**Problem:** Country selection dropdown overlapping with other elements, unusable interface
|
||||
**Status:** ✅ FIXED
|
||||
|
||||
---
|
||||
|
||||
## 🔍 **Root Causes & Solutions**
|
||||
|
|
@ -84,6 +88,42 @@ const setupPasswordUrl = computed(() => {
|
|||
});
|
||||
```
|
||||
|
||||
### **Country Dropdown Broken on Mobile**
|
||||
|
||||
#### **Root Causes:**
|
||||
1. **Vuetify v-select Issues:** Mobile Safari incompatibility with complex dropdown positioning
|
||||
2. **Z-index Conflicts:** Dropdown overlapping with other form elements
|
||||
3. **Touch Interaction Problems:** Poor touch responsiveness on mobile devices
|
||||
4. **Layout Disruption:** Dropdown breaking the form layout and rendering incorrectly
|
||||
|
||||
#### **Solutions Implemented:**
|
||||
```typescript
|
||||
// 1. Mobile-Optimized Country Selector
|
||||
components/MultipleNationalityInput.vue
|
||||
- Device detection to switch between desktop v-select and mobile dialog
|
||||
- Full-screen country selection dialog for mobile Safari
|
||||
- Touch-optimized interface with larger touch targets
|
||||
- Search functionality with smooth scrolling
|
||||
|
||||
// 2. Mobile Dialog Interface
|
||||
<v-dialog
|
||||
v-model="showMobileSelector"
|
||||
:fullscreen="useMobileInterface"
|
||||
:transition="'dialog-bottom-transition'"
|
||||
class="mobile-country-dialog"
|
||||
>
|
||||
<!-- Full-screen country list with search -->
|
||||
<!-- Optimized for touch interaction -->
|
||||
<!-- Smooth iOS-style animations -->
|
||||
</v-dialog>
|
||||
|
||||
// 3. Performance Optimizations
|
||||
- Hardware acceleration for smooth scrolling
|
||||
- Disabled transitions for performance mode
|
||||
- Touch-friendly 60px minimum button heights
|
||||
- -webkit-overflow-scrolling: touch for iOS
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 **Files Modified**
|
||||
|
|
|
|||
|
|
@ -6,7 +6,33 @@
|
|||
:key="`nationality-${index}`"
|
||||
class="nationality-item d-flex align-center gap-2 mb-2"
|
||||
>
|
||||
<!-- Mobile Safari optimized country selector -->
|
||||
<v-text-field
|
||||
v-if="useMobileInterface"
|
||||
:model-value="getSelectedCountryName(nationalities[index])"
|
||||
:label="index === 0 && label ? label : `Nationality ${index + 1}`"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
readonly
|
||||
:error="hasError && index === 0"
|
||||
:error-messages="hasError && index === 0 ? errorMessage : undefined"
|
||||
@click="openMobileSelector(index)"
|
||||
append-inner-icon="mdi-chevron-down"
|
||||
class="nationality-select mobile-optimized"
|
||||
>
|
||||
<template #prepend-inner v-if="nationalities[index]">
|
||||
<CountryFlag
|
||||
:country-code="nationalities[index]"
|
||||
:show-name="false"
|
||||
size="small"
|
||||
class="flag-icon me-2"
|
||||
/>
|
||||
</template>
|
||||
</v-text-field>
|
||||
|
||||
<!-- Traditional v-select for desktop -->
|
||||
<v-select
|
||||
v-else
|
||||
v-model="nationalities[index]"
|
||||
:items="countryOptions"
|
||||
:label="index === 0 && label ? label : `Nationality ${index + 1}`"
|
||||
|
|
@ -96,11 +122,104 @@
|
|||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile Safari Country Selection Dialog -->
|
||||
<v-dialog
|
||||
v-model="showMobileSelector"
|
||||
:fullscreen="useMobileInterface"
|
||||
:max-width="useMobileInterface ? undefined : '500px'"
|
||||
:transition="useMobileInterface ? 'dialog-bottom-transition' : 'dialog-transition'"
|
||||
class="mobile-country-dialog"
|
||||
>
|
||||
<v-card class="mobile-country-selector">
|
||||
<v-card-title class="d-flex align-center justify-space-between pa-4">
|
||||
<span class="text-h6">Select Country</span>
|
||||
<v-btn
|
||||
icon="mdi-close"
|
||||
variant="text"
|
||||
size="small"
|
||||
@click="showMobileSelector = false"
|
||||
/>
|
||||
</v-card-title>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<!-- Search field -->
|
||||
<div class="search-container pa-4 pb-2">
|
||||
<v-text-field
|
||||
v-model="searchQuery"
|
||||
placeholder="Search countries..."
|
||||
prepend-inner-icon="mdi-magnify"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details
|
||||
clearable
|
||||
class="country-search"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Country list -->
|
||||
<v-list class="country-list" density="comfortable">
|
||||
<template v-for="country in filteredCountries" :key="country.code">
|
||||
<v-list-item
|
||||
@click="selectCountry(country.code)"
|
||||
class="country-list-item"
|
||||
:class="{ 'selected': nationalities[currentEditingIndex] === country.code }"
|
||||
>
|
||||
<template #prepend>
|
||||
<div class="country-flag-container">
|
||||
<CountryFlag
|
||||
:country-code="country.code"
|
||||
:show-name="false"
|
||||
size="small"
|
||||
class="country-flag"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<v-list-item-title class="country-title">
|
||||
{{ country.name }}
|
||||
</v-list-item-title>
|
||||
|
||||
<template #append v-if="nationalities[currentEditingIndex] === country.code">
|
||||
<v-icon color="primary" size="small">mdi-check</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<v-list-item v-if="filteredCountries.length === 0" class="no-results">
|
||||
<v-list-item-title class="text-center text-medium-emphasis">
|
||||
No countries found
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
|
||||
<v-divider />
|
||||
|
||||
<v-card-actions class="pa-4">
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
variant="text"
|
||||
@click="showMobileSelector = false"
|
||||
class="text-none"
|
||||
>
|
||||
Cancel
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getAllCountries, getCountryName } from '~/utils/countries';
|
||||
import {
|
||||
getDeviceInfo,
|
||||
needsPerformanceOptimization,
|
||||
getOptimizedClasses
|
||||
} from '~/utils/mobile-safari-utils';
|
||||
|
||||
interface Props {
|
||||
modelValue?: string; // Comma-separated string like "FR,MC,US"
|
||||
|
|
@ -126,6 +245,11 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
// Mobile Safari detection
|
||||
const deviceInfo = ref(getDeviceInfo());
|
||||
const isMobileSafari = computed(() => deviceInfo.value.isMobileSafari);
|
||||
const needsPerformanceMode = computed(() => needsPerformanceOptimization());
|
||||
|
||||
// Parse initial nationalities from comma-separated string
|
||||
const parseNationalities = (value: string): string[] => {
|
||||
if (!value || value.trim() === '') return [''];
|
||||
|
|
@ -140,6 +264,26 @@ if (nationalities.value.length === 0) {
|
|||
nationalities.value = [''];
|
||||
}
|
||||
|
||||
// Mobile optimization flags
|
||||
const useMobileInterface = computed(() => isMobileSafari.value || needsPerformanceMode.value);
|
||||
|
||||
// Mobile dialog state
|
||||
const showMobileSelector = ref(false);
|
||||
const currentEditingIndex = ref(-1);
|
||||
const searchQuery = ref('');
|
||||
|
||||
// Filtered countries for mobile selector
|
||||
const filteredCountries = computed(() => {
|
||||
const countries = getAllCountries();
|
||||
if (!searchQuery.value) return countries;
|
||||
|
||||
const query = searchQuery.value.toLowerCase();
|
||||
return countries.filter(country =>
|
||||
country.name.toLowerCase().includes(query) ||
|
||||
country.code.toLowerCase().includes(query)
|
||||
);
|
||||
});
|
||||
|
||||
// Watch for external model changes
|
||||
watch(() => props.modelValue, (newValue) => {
|
||||
const newNationalities = parseNationalities(newValue || '');
|
||||
|
|
@ -200,6 +344,26 @@ const updateNationalities = () => {
|
|||
emit('update:modelValue', result);
|
||||
};
|
||||
|
||||
// Mobile Safari specific methods
|
||||
const getSelectedCountryName = (countryCode: string): string => {
|
||||
if (!countryCode) return '';
|
||||
return getCountryName(countryCode) || '';
|
||||
};
|
||||
|
||||
const openMobileSelector = (index: number) => {
|
||||
currentEditingIndex.value = index;
|
||||
showMobileSelector.value = true;
|
||||
};
|
||||
|
||||
const selectCountry = (countryCode: string) => {
|
||||
if (currentEditingIndex.value >= 0) {
|
||||
nationalities.value[currentEditingIndex.value] = countryCode;
|
||||
updateNationalities();
|
||||
}
|
||||
showMobileSelector.value = false;
|
||||
currentEditingIndex.value = -1;
|
||||
};
|
||||
|
||||
// Watch nationalities array for changes
|
||||
watch(nationalities, () => {
|
||||
updateNationalities();
|
||||
|
|
@ -393,4 +557,162 @@ onMounted(() => {
|
|||
:deep(.v-list-item[data-country="US"]) {
|
||||
background-color: rgba(var(--v-theme-primary), 0.02);
|
||||
}
|
||||
|
||||
/* Mobile Safari Country Dialog Styles */
|
||||
.mobile-country-dialog .v-dialog {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mobile-country-selector {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mobile-country-selector .v-card-text {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
flex-shrink: 0;
|
||||
border-bottom: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
.country-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
|
||||
max-height: calc(100vh - 200px);
|
||||
}
|
||||
|
||||
.country-list-item {
|
||||
min-height: 56px;
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.country-list-item:hover {
|
||||
background-color: rgba(var(--v-theme-primary), 0.08);
|
||||
}
|
||||
|
||||
.country-list-item.selected {
|
||||
background-color: rgba(var(--v-theme-primary), 0.12);
|
||||
}
|
||||
|
||||
.country-flag-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
min-width: 32px;
|
||||
height: 24px;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.country-flag {
|
||||
width: 24px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.country-title {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
color: rgba(var(--v-theme-on-surface), 0.87);
|
||||
}
|
||||
|
||||
.no-results {
|
||||
padding: 32px 16px;
|
||||
}
|
||||
|
||||
/* Mobile optimized text field */
|
||||
.nationality-select.mobile-optimized {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nationality-select.mobile-optimized :deep(.v-field__input) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nationality-select.mobile-optimized :deep(.v-field__field) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Mobile Safari specific fixes */
|
||||
@media (max-width: 768px) {
|
||||
.mobile-country-dialog :deep(.v-overlay__content) {
|
||||
margin: 0 !important;
|
||||
max-height: none !important;
|
||||
height: 100% !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.mobile-country-selector {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.country-list {
|
||||
max-height: calc(100vh - 160px);
|
||||
}
|
||||
|
||||
.country-list-item {
|
||||
min-height: 60px; /* Larger touch targets */
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.country-flag-container {
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
height: 27px;
|
||||
}
|
||||
|
||||
.country-flag {
|
||||
width: 28px;
|
||||
height: 21px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Performance optimizations for mobile Safari */
|
||||
.is-mobile-safari .mobile-country-selector,
|
||||
.performance-mode .mobile-country-selector {
|
||||
-webkit-transform: translateZ(0); /* Force hardware acceleration */
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
.is-mobile-safari .country-list,
|
||||
.performance-mode .country-list {
|
||||
will-change: scroll-position;
|
||||
}
|
||||
|
||||
.is-mobile-safari .country-list-item,
|
||||
.performance-mode .country-list-item {
|
||||
transition: none; /* Disable transitions for better performance */
|
||||
}
|
||||
|
||||
/* Smooth scrolling fix for mobile Safari */
|
||||
.mobile-country-dialog :deep(.v-overlay__scrim) {
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Fix dialog transition on mobile */
|
||||
@media (max-width: 768px) {
|
||||
.mobile-country-dialog :deep(.v-dialog-transition-enter-active),
|
||||
.mobile-country-dialog :deep(.v-dialog-transition-leave-active) {
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
|
||||
.mobile-country-dialog :deep(.v-dialog-transition-enter-from) {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.mobile-country-dialog :deep(.v-dialog-transition-leave-to) {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,337 @@
|
|||
<template>
|
||||
<div :class="containerClasses">
|
||||
<v-container class="fill-height" fluid>
|
||||
<v-row align="center" justify="center">
|
||||
<v-col cols="12" sm="8" md="6" lg="4">
|
||||
<v-card class="elevation-12 rounded-lg">
|
||||
<v-card-text class="text-center pa-8">
|
||||
<!-- Loading State -->
|
||||
<div v-if="verifying" class="mb-6">
|
||||
<v-progress-circular
|
||||
color="primary"
|
||||
size="80"
|
||||
width="6"
|
||||
indeterminate
|
||||
class="mb-4"
|
||||
/>
|
||||
|
||||
<h1 class="text-h4 font-weight-bold text-primary mb-3">
|
||||
Verifying Your Email
|
||||
</h1>
|
||||
|
||||
<p class="text-body-1 text-medium-emphasis">
|
||||
Please wait while we verify your email address...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Error State -->
|
||||
<div v-else-if="error" class="mb-6">
|
||||
<v-icon
|
||||
color="error"
|
||||
size="80"
|
||||
class="mb-4"
|
||||
>
|
||||
mdi-alert-circle
|
||||
</v-icon>
|
||||
|
||||
<h1 class="text-h4 font-weight-bold text-error mb-3">
|
||||
Verification Failed
|
||||
</h1>
|
||||
|
||||
<p class="text-body-1 text-medium-emphasis mb-4">
|
||||
{{ error }}
|
||||
</p>
|
||||
|
||||
<v-alert
|
||||
type="error"
|
||||
variant="tonal"
|
||||
class="mb-4 text-start"
|
||||
icon="mdi-information"
|
||||
>
|
||||
<div class="text-body-2">
|
||||
<strong>Common Issues:</strong>
|
||||
<ul class="mt-2">
|
||||
<li>The verification link may have expired</li>
|
||||
<li>The link may have already been used</li>
|
||||
<li>The link may be malformed</li>
|
||||
</ul>
|
||||
</div>
|
||||
</v-alert>
|
||||
</div>
|
||||
|
||||
<div v-if="!verifying" class="d-flex flex-column gap-3">
|
||||
<v-btn
|
||||
v-if="error"
|
||||
color="primary"
|
||||
size="large"
|
||||
variant="elevated"
|
||||
block
|
||||
@click="retryVerification"
|
||||
:loading="verifying"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start>mdi-refresh</v-icon>
|
||||
Retry Verification
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="secondary"
|
||||
size="large"
|
||||
variant="outlined"
|
||||
block
|
||||
to="/signup"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start>mdi-account-plus</v-icon>
|
||||
Register Again
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="outline"
|
||||
size="small"
|
||||
variant="text"
|
||||
block
|
||||
to="/"
|
||||
class="text-none"
|
||||
>
|
||||
<v-icon start>mdi-home</v-icon>
|
||||
Return to Home
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<!-- Additional help -->
|
||||
<div class="mt-6 pt-4 border-t">
|
||||
<p class="text-caption text-medium-emphasis mb-2">
|
||||
Need help? Contact support at:
|
||||
</p>
|
||||
<v-chip
|
||||
size="small"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-email"
|
||||
>
|
||||
support@monacousa.org
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
getOptimizedClasses,
|
||||
applyMobileSafariFixes
|
||||
} from '~/utils/mobile-safari-utils';
|
||||
|
||||
definePageMeta({
|
||||
layout: false,
|
||||
middleware: 'guest'
|
||||
});
|
||||
|
||||
// Mobile Safari optimization classes
|
||||
const containerClasses = computed(() => [
|
||||
'verification-page',
|
||||
...getOptimizedClasses()
|
||||
].join(' '));
|
||||
|
||||
// Reactive state
|
||||
const verifying = ref(true);
|
||||
const error = ref('');
|
||||
const route = useRoute();
|
||||
const token = computed(() => route.query.token as string || '');
|
||||
|
||||
// Set page title with mobile viewport optimization
|
||||
useHead({
|
||||
title: 'Verifying Email - MonacoUSA Portal',
|
||||
meta: [
|
||||
{
|
||||
name: 'description',
|
||||
content: 'Verifying your email address for the MonacoUSA Portal.'
|
||||
},
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no' }
|
||||
]
|
||||
});
|
||||
|
||||
// Verify email function
|
||||
const verifyEmail = async () => {
|
||||
if (!token.value) {
|
||||
error.value = 'No verification token provided. Please check your email for the correct verification link.';
|
||||
verifying.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
verifying.value = true;
|
||||
error.value = '';
|
||||
|
||||
// Call the API endpoint to verify the email
|
||||
const response = await $fetch(`/api/auth/verify-email?token=${token.value}`, {
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
console.log('[auth/verify] Email verification successful:', response);
|
||||
|
||||
// Redirect to success page with email info
|
||||
const email = route.query.email || '';
|
||||
const redirectUrl = `/auth/verify-success${email ? `?email=${encodeURIComponent(email as string)}` : ''}`;
|
||||
|
||||
await navigateTo(redirectUrl);
|
||||
|
||||
} catch (err: any) {
|
||||
console.error('[auth/verify] Email verification failed:', err);
|
||||
|
||||
if (err.statusCode === 400) {
|
||||
error.value = 'Invalid or expired verification token. Please request a new verification email.';
|
||||
} else if (err.statusCode === 404) {
|
||||
error.value = 'User not found. The verification token may be invalid.';
|
||||
} else if (err.statusCode === 409) {
|
||||
error.value = 'Email is already verified. You can now log in to your account.';
|
||||
} else {
|
||||
error.value = err.message || 'Email verification failed. Please try again or contact support.';
|
||||
}
|
||||
|
||||
verifying.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Retry verification
|
||||
const retryVerification = () => {
|
||||
verifyEmail();
|
||||
};
|
||||
|
||||
// Apply mobile Safari fixes and start verification
|
||||
onMounted(() => {
|
||||
// Apply mobile Safari fixes
|
||||
if (typeof window !== 'undefined') {
|
||||
applyMobileSafariFixes();
|
||||
}
|
||||
|
||||
console.log('[auth/verify] Starting email verification with token:', token.value?.substring(0, 20) + '...');
|
||||
|
||||
// Start verification process
|
||||
verifyEmail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.verification-page {
|
||||
min-height: 100vh;
|
||||
min-height: calc(var(--vh, 1vh) * 100); /* Mobile Safari fallback */
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
overflow-x: hidden; /* Prevent horizontal scroll on mobile */
|
||||
}
|
||||
|
||||
/* Mobile Safari optimizations */
|
||||
.verification-page.is-mobile-safari {
|
||||
min-height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.verification-page.performance-mode {
|
||||
will-change: auto;
|
||||
transform: translateZ(0); /* Lighter hardware acceleration */
|
||||
}
|
||||
|
||||
.fill-height {
|
||||
min-height: 100vh;
|
||||
min-height: calc(var(--vh, 1vh) * 100); /* Mobile Safari fallback */
|
||||
}
|
||||
|
||||
/* Mobile Safari fill-height optimization */
|
||||
.is-mobile-safari .fill-height {
|
||||
min-height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
||||
}
|
||||
|
||||
.gap-3 {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* Loading animation */
|
||||
.v-progress-circular {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.performance-mode .v-progress-circular {
|
||||
animation: none; /* Disable animations on performance mode */
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar for mobile */
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(163, 21, 21, 0.5);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 600px) {
|
||||
.verification-page {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.v-card {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Optimize button spacing on mobile */
|
||||
.gap-3 {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Improve touch targets on mobile */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.v-btn {
|
||||
min-height: 48px; /* Ensure touch-friendly button size */
|
||||
}
|
||||
}
|
||||
|
||||
/* Performance mode optimizations */
|
||||
.performance-mode .v-card {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important; /* Lighter shadow */
|
||||
}
|
||||
|
||||
.performance-mode .v-btn {
|
||||
transition: none; /* Remove button transitions for better performance */
|
||||
}
|
||||
|
||||
/* Error state styling */
|
||||
.text-error {
|
||||
color: rgb(var(--v-theme-error));
|
||||
}
|
||||
|
||||
/* Better list styling */
|
||||
ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4px 0;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -144,7 +144,8 @@ export default defineEventHandler(async (event) => {
|
|||
const emailService = await getEmailService();
|
||||
const verificationToken = await generateEmailVerificationToken(createdKeycloakId, body.email);
|
||||
const config = useRuntimeConfig();
|
||||
const verificationLink = `${config.public.domain}/api/auth/verify-email?token=${verificationToken}`;
|
||||
// Fix: Point to user-friendly verification page instead of direct API endpoint
|
||||
const verificationLink = `${config.public.domain}/auth/verify?token=${verificationToken}`;
|
||||
|
||||
await emailService.sendWelcomeEmail(body.email, {
|
||||
firstName: body.first_name,
|
||||
|
|
|
|||
Loading…
Reference in New Issue