Reflex/src/views/SwipeView.vue
2025-05-21 22:13:09 +07:00

302 lines
12 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="swipe-view container mt-4">
<h2 class="text-center mb-4">Найди свою пару!</h2>
<div v-if="loading" class="d-flex justify-content-center my-5">
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
<span class="visually-hidden">Загрузка анкет...</span>
</div>
</div>
<div v-if="error" class="alert alert-danger text-center">
{{ error }}
</div>
<!-- Сообщение, если нет предложений ПОСЛЕ загрузки и нет других ошибок -->
<div v-if="!loading && suggestions.length === 0 && !error" class="alert alert-info text-center">
Пока нет доступных анкет. Попробуй зайти позже!
</div>
<div v-if="currentUserToSwipe && !loading && !error" class="row justify-content-center">
<div class="col-md-6 col-lg-4">
<div class="card shadow-sm user-card">
<!-- === ОТОБРАЖЕНИЕ ФОТО === -->
<div class="image-placeholder-container">
<img
v-if="currentUserToSwipe.mainPhotoUrl"
:src="currentUserToSwipe.mainPhotoUrl"
class="card-img-top user-photo"
:alt="'Фото ' + currentUserToSwipe.name"
@error="onImageError($event, currentUserToSwipe)"
/>
<!-- Заглушка, если фото не загрузилось ИЛИ если mainPhotoUrl нет -->
<div v-else class="no-photo-placeholder d-flex align-items-center justify-content-center">
<i class="bi bi-person-fill"></i>
</div>
</div>
<!-- ========================== -->
<div class="card-body text-center">
<h5 class="card-title fs-3 mb-1">{{ currentUserToSwipe.name }}, {{ currentUserToSwipe.age !== null ? currentUserToSwipe.age : 'Возраст не указан' }}</h5>
<p class="card-text text-muted mb-2">{{ formatGender(currentUserToSwipe.gender) }}</p>
<p class="card-text bio-text">{{ currentUserToSwipe.bio || 'Нет описания.' }}</p>
<div class="d-flex justify-content-around mt-3 action-buttons-container">
<button @click="handlePass" class="btn btn-outline-danger btn-lg rounded-circle action-button" :disabled="actionLoading" aria-label="Пропустить">
<i class="bi bi-x-lg"></i>
</button>
<button @click="handleLike" class="btn btn-outline-success btn-lg rounded-circle action-button" :disabled="actionLoading" aria-label="Лайкнуть">
<i class="bi bi-heart-fill"></i>
</button>
</div>
</div>
</div>
<div v-if="matchOccurred" class="alert alert-success mt-3 text-center match-alert">
🎉 **Это мэтч!** Вы понравились друг другу! 🎉
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import api from '@/services/api'; // Наш API сервис
import { useAuth } from '@/auth'; // Для проверки аутентификации
import router from '@/router'; // Импортируем роутер для возможного редиректа
const { isAuthenticated, logout } = useAuth(); // Добавили 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 currentUserToSwipe = computed(() => {
if (suggestions.value.length > 0 && currentIndex.value < suggestions.value.length) {
return suggestions.value[currentIndex.value];
}
return null;
});
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);
if (suggestions.value.length === 0) {
console.log('[SwipeView] Нет доступных анкет.');
}
} 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) {
// Если токен невалиден, разлогиниваем и перенаправляем
// const { logout } = useAuth(); // logout уже импортирован выше
await logout(); // logout уже делает router.push('/login')
}
} 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;
setTimeout(() => {
matchOccurred.value = false;
// Переход к следующему после скрытия сообщения о мэтче
moveToNextUser();
}, 3000); // Показываем мэтч 3 секунды
} 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); // Показываем ошибку 3 секунды
}
// В случае ошибки (кроме 401) не переходим к следующему пользователю автоматически, даем шанс повторить
} finally {
actionLoading.value = false;
}
};
const handleLike = () => {
handleAction('like');
};
const handlePass = () => {
handleAction('pass');
};
const moveToNextUser = () => {
if (currentIndex.value < suggestions.value.length - 1) {
currentIndex.value++;
matchOccurred.value = false; // Убираем мэтч при переходе
} else {
console.log('[SwipeView] Предложения закончились, перезагружаем...');
fetchSuggestions();
}
};
onMounted(() => {
if (isAuthenticated.value) {
fetchSuggestions();
} else {
error.value = "Пожалуйста, войдите в систему, чтобы просматривать анкеты.";
loading.value = false;
// Перенаправление на логин должно осуществляться глобальным хуком роутера
// router.push('/login'); // Если хук не сработал или для явности
}
});
const formatGender = (gender) => {
if (gender === 'male') return 'Мужчина';
if (gender === 'female') return 'Женщина';
if (gender === 'other') return 'Другой';
return ''; // Возвращаем пустую строку, если пол не указан, чтобы не было "Пол не указан"
};
const onImageError = (event, userOnError) => {
console.warn("Не удалось загрузить изображение:", event.target.src, "для пользователя:", userOnError.name);
// Найдем пользователя в suggestions и установим mainPhotoUrl в null
// чтобы v-else сработал корректно и реактивно
const userInSuggestions = suggestions.value.find(u => u._id === userOnError._id);
if (userInSuggestions) {
userInSuggestions.mainPhotoUrl = null;
}
// event.target.style.display = 'none'; // Можно убрать, v-else должен справиться
};
</script>
<style scoped>
.swipe-view {
padding-bottom: 60px; /* Место для футера, если он фиксированный */
}
.user-card {
border-radius: 15px;
margin-bottom: 1.5rem;
overflow: hidden;
background-color: #fff;
}
.user-card .card-title {
font-weight: bold;
}
.user-card .bio-text {
min-height: 4.5em; /* Примерно 3 строки текста */
max-height: 4.5em;
line-height: 1.5em;
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3; /* Добавлено стандартное свойство */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 1rem;
}
.action-buttons-container {
padding-bottom: 0.5rem; /* Небольшой отступ снизу для кнопок */
}
.action-button {
width: 70px;
height: 70px;
font-size: 1.8rem; /* Размер иконки */
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
transition: transform 0.1s ease-out;
}
.action-button:active {
transform: scale(0.95);
}
.btn-outline-danger:hover {
background-color: #dc3545;
color: white;
}
.btn-outline-success:hover {
background-color: #198754;
color: white;
}
.match-alert {
position: fixed; /* Или absolute относительно родителя, если нужно */
bottom: 70px; /* Положение над предполагаемой панелью навигации/футером */
left: 50%;
transform: translateX(-50%);
z-index: 1050;
padding: 1rem 1.5rem;
font-size: 1.2rem;
animation: fadeInOut 3s forwards;
}
@keyframes fadeInOut {
0% { opacity: 0; transform: translateX(-50%) translateY(20px); }
20% { opacity: 1; transform: translateX(-50%) translateY(0); }
80% { opacity: 1; transform: translateX(-50%) translateY(0); }
100% { opacity: 0; transform: translateX(-50%) translateY(20px); }
}
.image-placeholder-container {
width: 100%;
padding-top: 100%;
position: relative;
background-color: #e9ecef; /* Более нейтральный фон для заглушки */
}
.user-photo, .no-photo-placeholder {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
.no-photo-placeholder {
border-bottom: 1px solid #dee2e6; /* Граница, как у card-img-top */
}
.no-photo-placeholder i {
font-size: 6rem; /* Увеличил иконку */
color: #adb5bd; /* Сделал иконку чуть темнее */
}
</style>