Reflex/src/views/SwipeView.vue
2025-05-23 20:45:40 +07:00

1223 lines
31 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-swipe-view">
<!-- Фиксированный хедер -->
<div class="swipe-header">
<h2 class="section-title">Поиск</h2>
</div>
<!-- Основное содержимое -->
<main class="swipe-content no-scroll">
<!-- Loading State -->
<div v-if="loading" class="loading-section">
<div class="loading-spinner">
<div class="spinner"></div>
<p>Загрузка анкет...</p>
</div>
</div>
<!-- Error State -->
<div v-if="error" class="error-section">
<div class="error-card">
<i class="bi-exclamation-triangle"></i>
<h3>Ошибка загрузки</h3>
<p>{{ error }}</p>
<button class="retry-btn" @click="fetchSuggestions">Попробовать снова</button>
</div>
</div>
<!-- No Suggestions State -->
<div v-if="!loading && suggestions.length === 0 && !error" class="empty-section">
<div class="empty-card">
<i class="bi-person-x"></i>
<h3>Нет анкет</h3>
<p>Пока нет доступных анкет. Попробуй зайти позже!</p>
</div>
</div>
<!-- Main Card Swiping Area -->
<div v-if="!loading && suggestions.length > 0 && !error" class="swipe-area" ref="swipeArea">
<!-- Card Stack -->
<div class="card-stack">
<div
v-for="(user, index) in visibleUsers"
:key="user._id"
class="swipe-card"
:class="{
'active': index === 0,
'next': index === 1,
'swiping-left': swipeDirection === 'left' && index === 0,
'swiping-right': swipeDirection === 'right' && index === 0
}"
:style="index === 0 ? cardStyle : {}"
@touchstart="index === 0 ? startTouch($event) : null"
@touchmove="index === 0 ? moveTouch($event) : null"
@touchend="index === 0 ? endTouch($event) : null"
@mousedown="index === 0 ? startDrag($event) : null"
>
<!-- Photo Section -->
<div class="card-photo-section">
<!-- Photo Carousel -->
<div class="photo-carousel">
<!-- Photos Array -->
<div
v-if="user.photos && user.photos.length > 0"
class="carousel-wrapper"
>
<div class="carousel-inner">
<div
v-for="(photo, photoIndex) in user.photos"
:key="photo.public_id || photo._id"
class="carousel-slide"
:class="{ 'active': currentPhotoIndex[user._id] === photoIndex }"
>
<img
:src="photo.url"
class="photo-image"
:alt="'Фото ' + user.name"
@error="onImageError($event, user, photo, 'carousel')"
/>
</div>
</div>
<!-- Photo Navigation Dots -->
<div class="carousel-dots" v-if="user.photos.length > 1">
<span
v-for="(photo, photoIndex) in user.photos"
:key="`dot-${photo._id}`"
class="dot"
:class="{ 'active': currentPhotoIndex[user._id] === photoIndex }"
@click.stop="changePhoto(user._id, photoIndex)"
></span>
</div>
<!-- Previous/Next buttons -->
<button
v-if="user.photos.length > 1"
class="carousel-nav prev"
@click.stop="prevPhoto(user._id)"
>
<i class="bi-chevron-left"></i>
</button>
<button
v-if="user.photos.length > 1"
class="carousel-nav next"
@click.stop="nextPhoto(user._id)"
>
<i class="bi-chevron-right"></i>
</button>
</div>
<!-- Single Photo -->
<div v-else-if="user.mainPhotoUrl" class="single-photo">
<img
:src="user.mainPhotoUrl"
class="photo-image"
:alt="'Фото ' + user.name"
@error="onImageError($event, user, { url: user.mainPhotoUrl }, 'mainPhoto')"
/>
</div>
<!-- No Photo Placeholder -->
<div v-else class="no-photo">
<i class="bi-person"></i>
</div>
</div>
<!-- Action Indicators -->
<div class="swipe-indicators">
<div
class="like-indicator"
:class="{ 'visible': swipeDirection === 'right' && index === 0 }"
>
<i class="bi-heart-fill"></i>
</div>
<div
class="pass-indicator"
:class="{ 'visible': swipeDirection === 'left' && index === 0 }"
>
<i class="bi-x-lg"></i>
</div>
</div>
<!-- User Info Overlay -->
<div class="user-info-overlay">
<h3 class="user-name">{{ user.name }}<span class="user-age">, {{ user.age || '?' }}</span></h3>
<p class="user-gender">{{ formatGender(user.gender) }}</p>
</div>
</div>
<!-- User Details Section -->
<div class="card-details-section">
<div class="bio-container">
<p class="bio-text">{{ user.bio || 'Нет описания' }}</p>
</div>
</div>
</div>
</div>
<!-- Убрали блок кнопок действий -->
</div>
</main>
<!-- Match Notification -->
<transition name="match-popup">
<div v-if="matchOccurred" class="match-popup">
<div class="match-content">
<div class="match-header">
<i class="bi-stars"></i>
<h3>Вы понравились друг другу!</h3>
</div>
<div class="match-actions">
<button class="match-btn secondary" @click="continueSearching">
Продолжить поиск
</button>
<button class="match-btn primary" @click="goToChats">
Начать общение
</button>
</div>
</div>
</div>
</transition>
<!-- Background Overlay for Match -->
<div
v-if="matchOccurred"
class="overlay"
@click="continueSearching"
></div>
</div>
</template>
<script setup>
import { ref, onMounted, computed, reactive, watch, onUnmounted, nextTick } from 'vue';
import api from '@/services/api';
import { useAuth } from '@/auth';
import router from '@/router';
const { isAuthenticated, logout } = useAuth();
// Основные данные
const suggestions = ref([]);
const currentIndex = ref(0);
const loading = ref(true);
const actionLoading = ref(false);
const error = ref('');
const matchOccurred = ref(false);
const matchedUserId = ref(null);
const swipeArea = ref(null);
// Для карусели фото
const currentPhotoIndex = reactive({});
// Для свайпа
const swipeDirection = ref(null);
const swipeThreshold = 80; // Минимальное расстояние для срабатывания свайпа
const cardStyle = ref({});
const swipeAngle = 12; // Угол наклона карты при свайпе (в градусах)
const isPanning = ref(false);
// Для touch событий
const touchStartX = ref(0);
const touchStartY = ref(0);
const touchEndX = ref(0);
const touchEndY = ref(0);
// Для mouse событий
const isDragging = ref(false);
const dragStartX = ref(0);
const dragStartY = ref(0);
const dragOffset = ref({ x: 0, y: 0 });
// Вычисляемые свойства
const currentUserToSwipe = computed(() => {
if (suggestions.value.length > 0 && currentIndex.value < suggestions.value.length) {
return suggestions.value[currentIndex.value];
}
return null;
});
const visibleUsers = computed(() => {
// Показываем текущего пользователя и следующего (для эффекта стека)
const users = [];
if (currentUserToSwipe.value) {
users.push(currentUserToSwipe.value);
if (currentIndex.value + 1 < suggestions.value.length) {
users.push(suggestions.value[currentIndex.value + 1]);
}
}
return users;
});
// Методы управления каруселью фото
const initPhotoIndices = () => {
suggestions.value.forEach(user => {
if (user && user._id) {
if (!currentPhotoIndex[user._id]) {
// Определяем начальное фото (основное или первое в списке)
if (user.photos && user.photos.length > 0) {
const profilePhotoIndex = user.photos.findIndex(p => p.isProfilePhoto);
currentPhotoIndex[user._id] = profilePhotoIndex >= 0 ? profilePhotoIndex : 0;
} else {
currentPhotoIndex[user._id] = 0;
}
}
}
});
};
const changePhoto = (userId, index) => {
if (isPanning.value) return; // Не меняем фото во время свайпа
currentPhotoIndex[userId] = index;
};
const nextPhoto = (userId) => {
if (isPanning.value) return;
const user = suggestions.value.find(u => u._id === userId);
if (user && user.photos && user.photos.length > 0) {
currentPhotoIndex[userId] = (currentPhotoIndex[userId] + 1) % user.photos.length;
}
};
const prevPhoto = (userId) => {
if (isPanning.value) return;
const user = suggestions.value.find(u => u._id === userId);
if (user && user.photos && user.photos.length > 0) {
currentPhotoIndex[userId] = (currentPhotoIndex[userId] - 1 + user.photos.length) % user.photos.length;
}
};
// Обработчики touch событий
const startTouch = (event) => {
isPanning.value = true;
touchStartX.value = event.touches[0].clientX;
touchStartY.value = event.touches[0].clientY;
// Сбросить конечные точки
touchEndX.value = 0;
touchEndY.value = 0;
};
const moveTouch = (event) => {
if (!isPanning.value || !touchStartX.value) return;
const currentX = event.touches[0].clientX;
const currentY = event.touches[0].clientY;
// Сохраняем текущую позицию
touchEndX.value = currentX;
touchEndY.value = currentY;
const diffX = currentX - touchStartX.value;
const diffY = currentY - touchStartY.value;
// Определяем направление свайпа для индикаторов
if (Math.abs(diffX) > Math.abs(diffY)) {
if (diffX > 20) {
swipeDirection.value = 'right';
} else if (diffX < -20) {
swipeDirection.value = 'left';
} else {
swipeDirection.value = null;
}
// Применяем небольшой поворот для естественности
const rotate = diffX * 0.05;
// Применяем точное смещение к карточке, она следует за пальцем 1 к 1
cardStyle.value = {
transform: `translateX(${diffX}px) rotate(${rotate}deg)`,
transition: 'none'
};
// Блокируем скролл страницы при горизонтальном свайпе
event.preventDefault();
}
};
const endTouch = (event) => {
if (!isPanning.value) return;
isPanning.value = false;
const diffX = touchEndX.value - touchStartX.value;
// Проверяем, достаточно ли было смещение для действия
if (Math.abs(diffX) > swipeThreshold) {
// Если свайп был достаточно сильным, выполняем соответствующее действие
if (diffX > 0) {
// Свайп вправо (лайк)
cardStyle.value = {
transform: `translateX(120%) rotate(${swipeAngle}deg)`,
transition: 'transform 0.3s ease'
};
// Плавная анимация уходящей карточки
setTimeout(() => handleLike(), 300);
} else {
// Свайп влево (пропуск)
cardStyle.value = {
transform: `translateX(-120%) rotate(-${swipeAngle}deg)`,
transition: 'transform 0.3s ease'
};
// Плавная анимация уходящей карточки
setTimeout(() => handlePass(), 300);
}
} else {
// Если свайп недостаточный, возвращаем карточку в исходное положение с анимацией
cardStyle.value = {
transform: 'none',
transition: 'transform 0.3s ease'
};
swipeDirection.value = null;
}
// Сбрасываем значения
touchStartX.value = 0;
touchStartY.value = 0;
touchEndX.value = 0;
touchEndY.value = 0;
};
// Обработчики mouse событий
const startDrag = (event) => {
isDragging.value = true;
dragStartX.value = event.clientX;
dragStartY.value = event.clientY;
// Сброс смещения
dragOffset.value = { x: 0, y: 0 };
// Добавляем слушатели событий мыши на document
document.addEventListener('mousemove', moveDrag);
document.addEventListener('mouseup', endDrag);
};
const moveDrag = (event) => {
if (!isDragging.value) return;
const currentX = event.clientX;
const currentY = event.clientY;
dragOffset.value = {
x: currentX - dragStartX.value,
y: currentY - dragStartY.value
};
// Определяем направление свайпа для индикаторов
if (Math.abs(dragOffset.value.x) > Math.abs(dragOffset.value.y)) {
if (dragOffset.value.x > 20) {
swipeDirection.value = 'right';
} else if (dragOffset.value.x < -20) {
swipeDirection.value = 'left';
} else {
swipeDirection.value = null;
}
// Применяем небольшой поворот для естественности
const rotate = dragOffset.value.x * 0.05;
// Точное следование за курсором
cardStyle.value = {
transform: `translateX(${dragOffset.value.x}px) rotate(${rotate}deg)`,
transition: 'none'
};
}
};
const endDrag = () => {
if (!isDragging.value) return;
// Проверяем, достаточно ли было смещение для действия
if (Math.abs(dragOffset.value.x) > swipeThreshold) {
// Если перетаскивание было достаточно большим, выполняем действие лайка/пропуска
if (dragOffset.value.x > 0) {
cardStyle.value = {
transform: `translateX(120%) rotate(${swipeAngle}deg)`,
transition: 'transform 0.3s ease'
};
setTimeout(() => handleLike(), 300);
} else {
cardStyle.value = {
transform: `translateX(-120%) rotate(-${swipeAngle}deg)`,
transition: 'transform 0.3s ease'
};
setTimeout(() => handlePass(), 300);
}
} else {
// Если перетаскивание небольшое, возвращаем карточку в исходное положение
cardStyle.value = {
transform: 'none',
transition: 'transform 0.3s ease'
};
swipeDirection.value = null;
}
// Сбрасываем состояние и удаляем слушатели
isDragging.value = false;
document.removeEventListener('mousemove', moveDrag);
document.removeEventListener('mouseup', endDrag);
};
// Основные функции
const fetchSuggestions = async () => {
loading.value = true;
error.value = '';
matchOccurred.value = false;
try {
console.log('[SwipeView] Запрос предложений...');
const response = await api.getSuggestions();
suggestions.value = response.data;
currentIndex.value = 0;
console.log('[SwipeView] Предложения загружены:', suggestions.value);
// Инициализируем индексы фото для карусели
nextTick(() => {
initPhotoIndices();
});
} catch (err) {
console.error('[SwipeView] Ошибка при загрузке предложений:', err.response ? err.response.data : err.message);
error.value = (err.response && err.response.data && err.response.data.message)
? err.response.data.message
: 'Не удалось загрузить анкеты. Попробуйте позже.';
if (err.response && err.response.status === 401) {
await logout();
}
} finally {
loading.value = false;
}
};
const handleAction = async (actionType) => {
if (!currentUserToSwipe.value || actionLoading.value) return;
actionLoading.value = true;
error.value = '';
if (actionType !== 'like') {
matchOccurred.value = false;
}
const targetUserId = currentUserToSwipe.value._id;
try {
let response;
if (actionType === 'like') {
console.log(`[SwipeView] Лайк пользователя: ${targetUserId}`);
response = await api.likeUser(targetUserId);
} else {
console.log(`[SwipeView] Пропуск пользователя: ${targetUserId}`);
response = await api.passUser(targetUserId);
}
console.log(`[SwipeView] Ответ на ${actionType}:`, response.data);
if (response.data.isMatch) {
matchOccurred.value = true;
matchedUserId.value = targetUserId;
} else {
moveToNextUser();
}
} catch (err) {
console.error(`[SwipeView] Ошибка при ${actionType}:`, err.response ? err.response.data : err.message);
error.value = `Ошибка при ${actionType === 'like' ? 'лайке' : 'пропуске'}. ${err.response?.data?.message || 'Попробуйте снова.'}`;
if (err.response && err.response.status === 401) {
await logout();
} else {
setTimeout(() => { error.value = ''; }, 3000);
}
} finally {
actionLoading.value = false;
}
};
const handleLike = () => {
// Анимация свайпа вправо
swipeDirection.value = 'right';
cardStyle.value = {
transform: `translateX(120%) rotate(${swipeAngle}deg)`,
transition: 'transform 0.5s ease'
};
// Задержка перед вызовом API
setTimeout(() => {
handleAction('like');
}, 300);
};
const handlePass = () => {
// Анимация свайпа влево
swipeDirection.value = 'left';
cardStyle.value = {
transform: `translateX(-120%) rotate(-${swipeAngle}deg)`,
transition: 'transform 0.5s ease'
};
// Задержка перед вызовом API
setTimeout(() => {
handleAction('pass');
}, 300);
};
const moveToNextUser = () => {
// Сбрасываем стили карточки
cardStyle.value = {
transform: 'none',
transition: 'none'
};
swipeDirection.value = null;
if (currentIndex.value < suggestions.value.length - 1) {
currentIndex.value++;
} else {
console.log('[SwipeView] Предложения закончились, перезагружаем...');
fetchSuggestions();
}
};
const continueSearching = () => {
matchOccurred.value = false;
moveToNextUser();
};
const goToChats = () => {
matchOccurred.value = false;
router.push('/chats');
};
const formatGender = (gender) => {
if (gender === 'male') return 'Мужчина';
if (gender === 'female') return 'Женщина';
if (gender === 'other') return 'Другой';
return '';
};
const onImageError = (event, userOnError, photoInfo, sourceType) => {
console.warn(
`SwipeView: Не удалось загрузить изображение (Источник: ${sourceType}).`,
{
src: event.target.src,
user: userOnError.name,
userId: userOnError._id,
photo_id: photoInfo._id,
photo_url: photoInfo.url,
isProfilePhotoFlag: photoInfo.isProfilePhoto,
event_type: event.type
}
);
};
// Обработчики событий жизненного цикла
onMounted(() => {
if (isAuthenticated.value) {
fetchSuggestions();
} else {
error.value = "Пожалуйста, войдите в систему, чтобы просматривать анкеты.";
loading.value = false;
}
});
onUnmounted(() => {
// Удаляем обработчики мыши при размонтировании компонента
document.removeEventListener('mousemove', moveDrag);
document.removeEventListener('mouseup', endDrag);
});
// Следим за изменениями в списке предложений
watch(suggestions, () => {
initPhotoIndices();
}, { deep: true });
</script>
<style scoped>
/* Основные стили контейнера */
.app-swipe-view {
display: flex;
flex-direction: column;
height: 100vh;
width: 100%;
background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: relative;
overflow: hidden;
}
/* Блокировка прокрутки */
.no-scroll {
overflow: hidden !important;
touch-action: none !important;
position: relative;
}
/* Хедер */
.swipe-header {
background: rgba(33, 33, 60, 0.9);
backdrop-filter: blur(10px);
padding: 0.8rem 0;
text-align: center;
color: white;
position: sticky;
top: 0;
z-index: 10;
height: var(--header-height, 56px);
display: flex;
align-items: center;
justify-content: center;
}
.section-title {
margin: 0;
font-size: 1.3rem;
font-weight: 600;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}
/* Основная область контента */
.swipe-content {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
padding-bottom: var(--tabs-height, 56px); /* Добавляем отступ снизу, чтобы учесть высоту табов */
}
/* Состояния загрузки и ошибки */
.loading-section,
.error-section,
.empty-section {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
padding: 1rem;
color: white;
}
.loading-spinner {
text-align: center;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-left: 4px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-card, .empty-card {
background: white;
border-radius: 20px;
padding: 2rem;
text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
max-width: 400px;
width: 100%;
}
.error-card i, .empty-card i {
font-size: 3rem;
margin-bottom: 1rem;
color: #495057;
}
.error-card h3, .empty-card h3 {
color: #343a40;
margin-bottom: 0.5rem;
}
.retry-btn {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
border: none;
padding: 0.8rem 2rem;
border-radius: 50px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
margin-top: 1rem;
display: inline-block;
}
/* Область свайпов */
.swipe-area {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
padding: 0.8rem;
touch-action: none;
}
/* Стек карточек */
.card-stack {
flex: 1;
position: relative;
margin: 0 auto;
width: 100%;
max-width: 400px;
perspective: 1000px;
}
/* Свайп-карточка */
.swipe-card {
position: absolute;
top: 0;
left: 0;
right: 0;
height: calc(100% - 16px); /* Ограничиваем высоту карточки, чтобы она не выходила за нижнюю границу */
border-radius: 16px;
background-color: white;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
overflow: hidden;
display: flex;
flex-direction: column;
will-change: transform;
transition: transform 0.3s ease, opacity 0.3s ease, scale 0.3s ease;
}
.swipe-card.active {
z-index: 10;
}
.swipe-card.next {
z-index: 5;
transform: scale(0.95) translateY(15px);
opacity: 0.8;
}
/* Секция с фотографией */
.card-photo-section {
flex: 1;
position: relative;
min-height: 0;
max-height: 70vh;
overflow: hidden;
}
.photo-carousel {
width: 100%;
height: 100%;
position: relative;
}
.carousel-wrapper {
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.carousel-inner {
width: 100%;
height: 100%;
position: relative;
}
.carousel-slide {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 0.5s ease;
}
.carousel-slide.active {
opacity: 1;
}
.photo-image {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Навигация карусели */
.carousel-nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: rgba(255, 255, 255, 0.7);
border: none;
width: 36px;
height: 36px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
z-index: 5;
transition: all 0.2s ease;
}
.carousel-nav.prev {
left: 12px;
}
.carousel-nav.next {
right: 12px;
}
.carousel-dots {
position: absolute;
bottom: 15px;
left: 0;
right: 0;
display: flex;
justify-content: center;
gap: 6px;
z-index: 5;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.5);
cursor: pointer;
transition: all 0.2s ease;
}
.dot.active {
background-color: white;
transform: scale(1.2);
}
/* Заполнители для фото */
.single-photo {
width: 100%;
height: 100%;
}
.no-photo {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #e9ecef;
}
.no-photo i {
font-size: 4rem;
color: #adb5bd;
}
/* Индикаторы действий */
.swipe-indicators {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.like-indicator, .pass-indicator {
position: absolute;
top: 50%;
transform: translateY(-50%) scale(0);
display: flex;
align-items: center;
justify-content: center;
width: 90px;
height: 90px;
border-radius: 50%;
font-size: 2.8rem;
opacity: 0;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.like-indicator {
right: 20%;
background-color: rgba(40, 167, 69, 0.8);
color: white;
}
.pass-indicator {
left: 20%;
background-color: rgba(220, 53, 69, 0.8);
color: white;
}
.like-indicator.visible, .pass-indicator.visible {
opacity: 1;
transform: translateY(-50%) scale(1);
}
/* Информация о пользователе */
.user-info-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
color: white;
padding: 16px;
text-align: left;
}
.user-name {
margin: 0;
font-size: 1.6rem;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
.user-gender {
margin: 5px 0 0;
opacity: 0.9;
}
/* Секция с деталями */
.card-details-section {
background: white;
padding: 12px 16px;
border-top: 1px solid rgba(0, 0, 0, 0.05);
flex-shrink: 0;
max-height: 100px; /* Ограничиваем высоту секции с деталями */
overflow-y: auto; /* Добавляем прокрутку, если текст не помещается */
}
/* Кнопки действий */
.action-buttons {
display: flex;
justify-content: center;
gap: 20px;
padding: 16px 0;
}
.action-btn {
width: 65px;
height: 65px;
border-radius: 50%;
border: none;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
background: white;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
}
.btn-icon {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
}
.pass-btn .btn-icon {
color: #dc3545;
}
.like-btn .btn-icon {
color: #28a745;
}
.action-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
/* Всплывающее окно совпадения */
.match-popup {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border-radius: 20px;
padding: 1.8rem;
width: 90%;
max-width: 360px;
box-shadow: 0 15px 40px rgba(0, 0, 0, 0.3);
text-align: center;
z-index: 1000;
}
.match-header {
margin-bottom: 1.5rem;
}
.match-header i {
font-size: 3rem;
color: #ff6b6b;
margin-bottom: 1rem;
}
.match-header h3 {
font-size: 1.4rem;
color: #333;
margin: 0;
}
.match-actions {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.match-btn {
padding: 0.8rem 1rem;
border-radius: 50px;
font-weight: 600;
border: none;
cursor: pointer;
transition: all 0.3s ease;
width: 100%;
}
.match-btn.primary {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
}
.match-btn.secondary {
background: #f1f3f5;
color: #495057;
}
/* Затемняющий оверлей */
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
z-index: 999;
}
/* Анимации для всплывающего окна */
.match-popup-enter-active, .match-popup-leave-active {
transition: all 0.3s ease;
}
.match-popup-enter-from, .match-popup-leave-to {
transform: translate(-50%, -50%) scale(0.9);
opacity: 0;
}
/* Адаптивные настройки */
@media (min-height: 700px) {
.card-photo-section {
max-height: 65vh;
}
}
@media (max-height: 600px) {
.card-photo-section {
max-height: 60vh;
}
.card-details-section {
padding: 8px 12px;
}
.bio-container {
max-height: 60px;
}
.action-buttons {
padding: 10px 0;
}
.action-btn {
width: 55px;
height: 55px;
}
.btn-icon {
font-size: 1.5rem;
}
}
/* Ландшафтная ориентация */
@media (max-height: 450px) and (orientation: landscape) {
.swipe-area {
flex-direction: row;
gap: 0.8rem;
padding: 0.5rem 0.8rem;
}
.card-stack {
max-width: calc(100% - 130px);
}
.action-buttons {
flex-direction: column;
height: 100%;
padding: 16px 0;
justify-content: center;
}
.swipe-header {
height: 46px;
padding: 0.5rem 0;
}
.section-title {
font-size: 1.1rem;
}
.card-details-section {
padding: 6px 10px;
}
.bio-container {
max-height: 50px;
}
.user-name {
font-size: 1.3rem;
}
}
/* Маленькие экраны */
@media (max-width: 375px) {
.user-name {
font-size: 1.4rem;
}
.action-btn {
width: 60px;
height: 60px;
}
.carousel-nav {
width: 32px;
height: 32px;
}
.like-indicator, .pass-indicator {
width: 80px;
height: 80px;
font-size: 2.5rem;
}
}
/* Учитываем Safe Area для iPhone X+ */
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.action-buttons {
padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px));
}
}
</style>