2025-08-09 18:53:26 +02:00
|
|
|
<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">
|
2025-08-10 15:48:11 +02:00
|
|
|
<!-- Circuit Breaker - Too Many Attempts -->
|
|
|
|
|
<div v-if="isBlocked" class="mb-6">
|
|
|
|
|
<v-icon
|
|
|
|
|
color="warning"
|
|
|
|
|
size="80"
|
|
|
|
|
class="mb-4"
|
|
|
|
|
>
|
|
|
|
|
mdi-timer-sand
|
|
|
|
|
</v-icon>
|
|
|
|
|
|
|
|
|
|
<h1 class="text-h4 font-weight-bold text-warning mb-3">
|
|
|
|
|
Verification Temporarily Blocked
|
|
|
|
|
</h1>
|
|
|
|
|
|
|
|
|
|
<p class="text-body-1 text-medium-emphasis mb-4">
|
|
|
|
|
{{ statusMessage }}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<v-alert
|
|
|
|
|
type="warning"
|
|
|
|
|
variant="tonal"
|
|
|
|
|
class="mb-4 text-start"
|
|
|
|
|
icon="mdi-information"
|
|
|
|
|
>
|
|
|
|
|
<div class="text-body-2">
|
|
|
|
|
<strong>Why was this blocked?</strong>
|
|
|
|
|
<ul class="mt-2">
|
|
|
|
|
<li>Multiple failed verification attempts detected</li>
|
|
|
|
|
<li>This prevents server overload and potential issues</li>
|
|
|
|
|
<li>The block will be lifted automatically</li>
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</v-alert>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-08-09 18:53:26 +02:00
|
|
|
<!-- Loading State -->
|
2025-08-10 15:48:11 +02:00
|
|
|
<div v-else-if="verifying" class="mb-6">
|
2025-08-09 18:53:26 +02:00
|
|
|
<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>
|
|
|
|
|
|
2025-08-14 15:08:40 +02:00
|
|
|
<p class="text-body-1 text-medium-emphasis">
|
2025-08-10 15:48:11 +02:00
|
|
|
{{ statusMessage || 'Please wait while we verify your email address...' }}
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<!-- Attempt Counter -->
|
2025-08-14 15:08:40 +02:00
|
|
|
<div v-if="attemptCount > 1" class="mt-2">
|
2025-08-10 15:48:11 +02:00
|
|
|
<v-chip size="small" color="primary" variant="outlined">
|
2025-08-14 15:08:40 +02:00
|
|
|
Attempt {{ attemptCount }}/{{ maxAttempts }}
|
2025-08-10 15:48:11 +02:00
|
|
|
</v-chip>
|
|
|
|
|
</div>
|
2025-08-09 18:53:26 +02:00
|
|
|
</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>
|
2025-08-10 15:48:11 +02:00
|
|
|
|
|
|
|
|
<!-- Circuit Breaker Status -->
|
2025-08-14 15:08:40 +02:00
|
|
|
<div v-if="statusMessage" class="mb-4">
|
2025-08-10 15:48:11 +02:00
|
|
|
<v-alert
|
|
|
|
|
type="info"
|
|
|
|
|
variant="tonal"
|
|
|
|
|
class="text-start"
|
|
|
|
|
icon="mdi-information"
|
|
|
|
|
>
|
|
|
|
|
{{ statusMessage }}
|
|
|
|
|
</v-alert>
|
|
|
|
|
</div>
|
2025-08-09 18:53:26 +02:00
|
|
|
|
|
|
|
|
<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>
|
2025-08-10 15:48:11 +02:00
|
|
|
<li v-if="partialSuccess">Server configuration issues (contact support)</li>
|
2025-08-09 18:53:26 +02:00
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
</v-alert>
|
|
|
|
|
</div>
|
|
|
|
|
|
2025-08-10 15:48:11 +02:00
|
|
|
<div v-if="!verifying && !isBlocked" class="d-flex flex-column gap-3">
|
2025-08-09 18:53:26 +02:00
|
|
|
<v-btn
|
2025-08-10 15:48:11 +02:00
|
|
|
v-if="error && canRetry"
|
2025-08-09 18:53:26 +02:00
|
|
|
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>
|
|
|
|
|
|
2025-08-10 15:48:11 +02:00
|
|
|
<div v-else-if="isBlocked" class="d-flex flex-column gap-3">
|
|
|
|
|
<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>
|
|
|
|
|
|
2025-08-09 18:53:26 +02:00
|
|
|
<!-- 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">
|
|
|
|
|
definePageMeta({
|
|
|
|
|
layout: false,
|
|
|
|
|
middleware: 'guest'
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-09 20:20:26 +02:00
|
|
|
// Get route and token immediately
|
|
|
|
|
const route = useRoute();
|
|
|
|
|
const token = route.query.token as string || '';
|
2025-08-09 18:53:26 +02:00
|
|
|
|
2025-08-09 20:20:26 +02:00
|
|
|
// Reactive state - keep minimal reactivity
|
2025-08-10 15:48:11 +02:00
|
|
|
const verifying = ref(false);
|
2025-08-09 18:53:26 +02:00
|
|
|
const error = ref('');
|
2025-08-10 15:48:11 +02:00
|
|
|
const partialSuccess = ref(false);
|
2025-08-09 20:20:26 +02:00
|
|
|
|
2025-08-14 15:08:40 +02:00
|
|
|
// Simple retry logic
|
2025-08-10 15:48:11 +02:00
|
|
|
const isBlocked = ref(false);
|
|
|
|
|
const canRetry = ref(true);
|
|
|
|
|
const statusMessage = ref('');
|
2025-08-14 15:08:40 +02:00
|
|
|
const attemptCount = ref(0);
|
|
|
|
|
const maxAttempts = 3;
|
|
|
|
|
|
|
|
|
|
// Device detection
|
|
|
|
|
const isMobile = ref(false);
|
|
|
|
|
const isMobileSafari = ref(false);
|
|
|
|
|
|
|
|
|
|
// Initialize device detection on mount
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
if (process.client) {
|
|
|
|
|
const userAgent = navigator.userAgent;
|
|
|
|
|
isMobile.value = /Android|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) || window.innerWidth <= 768;
|
|
|
|
|
isMobileSafari.value = /iPhone|iPad|iPod/i.test(userAgent) && /Safari/i.test(userAgent);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-08-10 15:18:34 +02:00
|
|
|
|
2025-08-14 15:08:40 +02:00
|
|
|
// CSS classes based on device detection
|
|
|
|
|
const containerClasses = computed(() => {
|
|
|
|
|
const classes = ['verification-page'];
|
|
|
|
|
if (isMobile.value) classes.push('is-mobile');
|
|
|
|
|
if (isMobileSafari.value) classes.push('is-mobile-safari', 'performance-mode');
|
|
|
|
|
return classes.join(' ');
|
|
|
|
|
});
|
2025-08-09 18:53:26 +02:00
|
|
|
|
|
|
|
|
// 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.'
|
|
|
|
|
},
|
2025-08-14 15:08:40 +02:00
|
|
|
{
|
|
|
|
|
name: 'viewport',
|
|
|
|
|
content: 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover'
|
|
|
|
|
}
|
2025-08-09 18:53:26 +02:00
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
|
2025-08-14 15:08:40 +02:00
|
|
|
// Simple verification logic
|
2025-08-10 15:48:11 +02:00
|
|
|
const updateUIState = () => {
|
2025-08-14 15:08:40 +02:00
|
|
|
if (attemptCount.value >= maxAttempts) {
|
|
|
|
|
isBlocked.value = true;
|
|
|
|
|
canRetry.value = false;
|
|
|
|
|
statusMessage.value = `Too many failed attempts. Please wait before trying again.`;
|
|
|
|
|
} else {
|
|
|
|
|
canRetry.value = attemptCount.value < maxAttempts;
|
|
|
|
|
}
|
2025-08-10 15:48:11 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-14 15:08:40 +02:00
|
|
|
// Verify email function
|
2025-08-09 18:53:26 +02:00
|
|
|
const verifyEmail = async () => {
|
2025-08-09 20:20:26 +02:00
|
|
|
if (!token) {
|
2025-08-09 18:53:26 +02:00
|
|
|
error.value = 'No verification token provided. Please check your email for the correct verification link.';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-14 15:08:40 +02:00
|
|
|
if (attemptCount.value >= maxAttempts) {
|
|
|
|
|
isBlocked.value = true;
|
2025-08-10 15:48:11 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-09 18:53:26 +02:00
|
|
|
try {
|
|
|
|
|
verifying.value = true;
|
|
|
|
|
error.value = '';
|
2025-08-10 15:48:11 +02:00
|
|
|
partialSuccess.value = false;
|
2025-08-14 15:08:40 +02:00
|
|
|
attemptCount.value++;
|
|
|
|
|
|
|
|
|
|
console.log(`[auth/verify] Starting verification attempt ${attemptCount.value}/${maxAttempts}`);
|
2025-08-09 20:20:26 +02:00
|
|
|
|
2025-08-09 18:53:26 +02:00
|
|
|
// Call the API endpoint to verify the email
|
2025-08-09 20:20:26 +02:00
|
|
|
const response = await $fetch(`/api/auth/verify-email?token=${token}`, {
|
2025-08-09 18:53:26 +02:00
|
|
|
method: 'GET'
|
2025-08-09 19:40:04 +02:00
|
|
|
}) as any;
|
2025-08-09 18:53:26 +02:00
|
|
|
|
|
|
|
|
console.log('[auth/verify] Email verification successful:', response);
|
|
|
|
|
|
2025-08-10 15:48:11 +02:00
|
|
|
// Extract response data
|
2025-08-09 19:40:04 +02:00
|
|
|
const email = response?.data?.email || '';
|
2025-08-10 15:48:11 +02:00
|
|
|
const isPartialSuccess = response?.data?.partialSuccess || false;
|
|
|
|
|
const keycloakError = response?.data?.keycloakError;
|
|
|
|
|
|
|
|
|
|
if (isPartialSuccess) {
|
|
|
|
|
partialSuccess.value = true;
|
|
|
|
|
console.log('[auth/verify] Partial success - Keycloak error:', keycloakError);
|
|
|
|
|
}
|
2025-08-09 20:20:26 +02:00
|
|
|
|
2025-08-10 15:48:11 +02:00
|
|
|
// Construct redirect URL
|
2025-08-09 19:40:04 +02:00
|
|
|
let redirectUrl = `/auth/verify-success`;
|
|
|
|
|
const queryParams = [];
|
|
|
|
|
|
|
|
|
|
if (email) {
|
|
|
|
|
queryParams.push(`email=${encodeURIComponent(email)}`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-10 15:48:11 +02:00
|
|
|
if (isPartialSuccess) {
|
2025-08-09 19:40:04 +02:00
|
|
|
queryParams.push('warning=partial');
|
2025-08-10 15:48:11 +02:00
|
|
|
if (keycloakError) {
|
|
|
|
|
queryParams.push(`error=${encodeURIComponent(keycloakError)}`);
|
|
|
|
|
}
|
2025-08-09 19:40:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (queryParams.length > 0) {
|
|
|
|
|
redirectUrl += '?' + queryParams.join('&');
|
|
|
|
|
}
|
2025-08-09 18:53:26 +02:00
|
|
|
|
2025-08-14 15:08:40 +02:00
|
|
|
// Navigate to success page
|
|
|
|
|
console.log(`[auth/verify] Navigating to success page`);
|
2025-08-10 15:48:11 +02:00
|
|
|
|
|
|
|
|
setTimeout(async () => {
|
|
|
|
|
try {
|
2025-08-14 15:08:40 +02:00
|
|
|
await navigateTo(redirectUrl, { replace: true });
|
2025-08-10 15:48:11 +02:00
|
|
|
} catch (navError) {
|
|
|
|
|
console.error('[auth/verify] Navigation failed:', navError);
|
|
|
|
|
// Final fallback - direct window location
|
|
|
|
|
window.location.replace(redirectUrl);
|
|
|
|
|
}
|
2025-08-14 15:08:40 +02:00
|
|
|
}, 500);
|
2025-08-09 18:53:26 +02:00
|
|
|
|
|
|
|
|
} catch (err: any) {
|
|
|
|
|
console.error('[auth/verify] Email verification failed:', err);
|
|
|
|
|
|
2025-08-10 15:48:11 +02:00
|
|
|
updateUIState();
|
|
|
|
|
|
|
|
|
|
// Set error message based on status code
|
2025-08-09 19:40:04 +02:00
|
|
|
if (err.statusCode === 410) {
|
|
|
|
|
error.value = 'Verification link has expired. Please request a new verification email.';
|
|
|
|
|
} else if (err.statusCode === 409) {
|
|
|
|
|
error.value = 'This verification link has already been used or is invalid.';
|
|
|
|
|
} else if (err.statusCode === 400) {
|
|
|
|
|
error.value = 'Invalid verification token. Please request a new verification email.';
|
2025-08-09 18:53:26 +02:00
|
|
|
} else if (err.statusCode === 404) {
|
|
|
|
|
error.value = 'User not found. The verification token may be invalid.';
|
|
|
|
|
} else {
|
2025-08-14 15:08:40 +02:00
|
|
|
error.value = err.data?.message || err.message || 'Email verification failed';
|
2025-08-09 18:53:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
verifying.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-08-10 15:48:11 +02:00
|
|
|
// Retry verification
|
|
|
|
|
const retryVerification = async () => {
|
|
|
|
|
if (!canRetry.value || isBlocked.value) {
|
|
|
|
|
console.log('[auth/verify] Retry blocked - canRetry:', canRetry.value, 'isBlocked:', isBlocked.value);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log('[auth/verify] Retrying verification...');
|
|
|
|
|
await verifyEmail();
|
2025-08-09 18:53:26 +02:00
|
|
|
};
|
|
|
|
|
|
2025-08-14 15:08:40 +02:00
|
|
|
// Component initialization
|
2025-08-10 16:21:54 +02:00
|
|
|
onMounted(async () => {
|
2025-08-09 20:20:26 +02:00
|
|
|
console.log('[auth/verify] Component mounted with token:', token?.substring(0, 20) + '...');
|
2025-08-10 15:18:34 +02:00
|
|
|
|
2025-08-10 15:48:11 +02:00
|
|
|
// Check if token exists
|
|
|
|
|
if (!token) {
|
|
|
|
|
error.value = 'No verification token provided. Please check your email for the correct verification link.';
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-08-09 20:20:26 +02:00
|
|
|
|
|
|
|
|
// Start verification process with a small delay to ensure stability
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
verifyEmail();
|
2025-08-10 15:48:11 +02:00
|
|
|
}, 200);
|
2025-08-09 18:53:26 +02:00
|
|
|
});
|
|
|
|
|
</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>
|