реорганизация приложения

This commit is contained in:
Professional 2025-05-23 18:42:29 +07:00
parent 2c452f9f20
commit c61a15c8a7
4 changed files with 1258 additions and 1736 deletions

View File

@ -1,157 +1,344 @@
<template> <template>
<div id="app-container"> <div id="app-container">
<header class="app-header bg-dark text-white p-3 mb-4 shadow-sm"> <!-- Основная область с содержимым -->
<nav class="container d-flex justify-content-between align-items-center"> <main class="app-content">
<router-link to="/" class="navbar-brand text-white fs-4">DatingApp</router-link> <router-view v-slot="{ Component }">
<div> <transition name="page-transition" mode="out-in">
<template v-if="authState"> <component :is="Component" />
<span class="me-3">Привет, {{ userData?.name || 'Пользователь' }}!</span> </transition>
<router-link to="/swipe" class="btn btn-outline-light me-2">Поиск</router-link> </router-view>
<router-link to="/chats" class="btn btn-outline-light me-2">Чаты</router-link> <!-- НОВАЯ ССЫЛКА -->
<router-link to="/profile" class="btn btn-outline-light me-2">Профиль</router-link>
<button @click="handleLogout" class="btn btn-light">Выход</button>
</template>
<template v-else>
<router-link to="/login" class="btn btn-outline-light me-2">Войти</router-link>
<router-link to="/register" class="btn btn-light">Регистрация</router-link>
</template>
</div>
</nav>
</header>
<main class="container">
<router-view /> <!-- Сюда Vue Router будет рендерить компонент текущего маршрута -->
</main> </main>
<footer class="app-footer mt-auto py-3 bg-light text-center"> <!-- Мобильная навигация внизу экрана -->
<div class="container"> <nav v-if="authState" class="mobile-nav">
<span class="text-muted">© {{ new Date().getFullYear() }} Dating App PWA</span> <router-link to="/swipe" class="nav-item" active-class="active">
<div class="nav-icon">
<i class="bi-search"></i>
</div> </div>
</footer> <span>Поиск</span>
</router-link>
<router-link to="/chats" class="nav-item" active-class="active">
<div class="nav-icon">
<i class="bi-chat-dots"></i>
<span v-if="totalUnread > 0" class="unread-badge">
{{ totalUnread < 100 ? totalUnread : '99+' }}
</span>
</div>
<span>Чаты</span>
</router-link>
<router-link to="/profile" class="nav-item" active-class="active">
<div class="nav-icon">
<i class="bi-person"></i>
</div>
<span>Профиль</span>
</router-link>
</nav>
</div> </div>
</template> </template>
<script setup> <script setup>
import { useAuth } from '@/auth'; // Убедись, что путь '@/auth' правильный import { useAuth } from '@/auth';
import { ref, watch, onMounted } from 'vue'; import { ref, watch, onMounted, computed } from 'vue';
import api from '@/services/api';
import { getSocket, connectSocket } from '@/services/socketService';
const { user, isAuthenticated, logout, fetchUser } = useAuth(); const { user, isAuthenticated, logout, fetchUser } = useAuth();
// Используем локальные переменные для отслеживания состояния // Используем локальные переменные для отслеживания состояния
const userData = ref(null); const userData = ref(null);
const authState = ref(false); const authState = ref(false);
const conversations = ref([]);
const totalUnread = computed(() => {
return conversations.value.reduce((sum, conv) => sum + (conv.unreadCount || 0), 0);
});
let socket = null;
// Получение списка диалогов для отображения счетчика непрочитанных сообщений
const fetchConversations = async () => {
if (!isAuthenticated.value) return;
try {
const response = await api.getConversations();
conversations.value = response.data;
console.log('[App] Загружено диалогов для счетчика уведомлений:', conversations.value.length);
} catch (err) {
console.error('[App] Ошибка при загрузке диалогов:', err);
}
};
// Обработчик новых сообщений
const handleNewMessage = (message) => {
// Если сообщение от другого пользователя, увеличиваем счетчик
if (message.sender !== user.value?._id) {
const conversation = conversations.value.find(c => c._id === message.conversationId);
if (conversation) {
conversation.unreadCount = (conversation.unreadCount || 0) + 1;
} else {
// Если диалога нет в списке, обновим весь список
fetchConversations();
}
}
};
// Обработчик прочитанных сообщений
const handleMessagesRead = ({ conversationId, readerId }) => {
// Если текущий пользователь прочитал сообщения
if (readerId === user.value?._id) {
const conversation = conversations.value.find(c => c._id === conversationId);
if (conversation) {
conversation.unreadCount = 0;
}
}
};
// Обновляем локальные переменные при изменении состояния аутентификации // Обновляем локальные переменные при изменении состояния аутентификации
watch(() => isAuthenticated.value, (newVal) => { watch(() => isAuthenticated.value, (newVal) => {
console.log('isAuthenticated изменился:', newVal); console.log('[App] isAuthenticated изменился:', newVal);
authState.value = newVal; authState.value = newVal;
if (newVal) {
fetchConversations();
setupSocketConnection();
}
}); });
// Обновляем локальную копию данных пользователя // Обновляем локальную копию данных пользователя
watch(() => user.value, (newVal) => { watch(() => user.value, (newVal) => {
console.log('Данные пользователя изменились:', newVal); console.log('[App] Данные пользователя изменились:', newVal);
userData.value = newVal; userData.value = newVal;
}); });
// Настройка сокет-соединения
const setupSocketConnection = () => {
socket = getSocket();
if (!socket) {
socket = connectSocket();
}
if (socket) {
socket.on('getMessage', handleNewMessage);
socket.on('messagesRead', handleMessagesRead);
// Присоединение к комнате для уведомлений
if (user.value?._id) {
socket.emit('joinNotificationRoom', user.value._id);
}
}
};
// При монтировании компонента синхронизируем состояние // При монтировании компонента синхронизируем состояние
onMounted(() => { onMounted(() => {
console.log('App.vue монтирован, проверка состояния аутентификации'); console.log('[App] App.vue монтирован, проверка состояния аутентификации');
authState.value = isAuthenticated.value; authState.value = isAuthenticated.value;
userData.value = user.value; userData.value = user.value;
// Убедимся, что у нас есть актуальные данные пользователя // Убедимся, что у нас есть актуальные данные пользователя
if (localStorage.getItem('userToken') && !isAuthenticated.value) { if (localStorage.getItem('userToken') && !isAuthenticated.value) {
fetchUser(); fetchUser().then(() => {
if (isAuthenticated.value) {
fetchConversations();
setupSocketConnection();
}
});
} else if (isAuthenticated.value) {
fetchConversations();
setupSocketConnection();
} }
}); });
const handleLogout = async () => { // При размонтировании компонента очищаем обработчики событий
try { onUnmounted(() => {
await logout(); if (socket) {
console.log('Пользователь успешно вышел из системы (из App.vue)'); socket.off('getMessage', handleNewMessage);
authState.value = false; socket.off('messagesRead', handleMessagesRead);
userData.value = null;
} catch (error) { if (user.value?._id) {
console.error('Ошибка при выходе:', error); socket.emit('leaveNotificationRoom', user.value._id);
} }
}; }
});
</script> </script>
<style> <style>
/* Глобальные стили */ /* Глобальные стили */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body { html, body {
height: 100%; height: 100%;
width: 100%;
overflow-x: hidden;
-webkit-tap-highlight-color: transparent; /* Убирает фон при касании на мобильных */
} }
body { body {
margin: 0; margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: #212529; color: #212529;
background-color: #f8f9fa; /* Светлый фон для всего приложения */ background-color: #f8f9fa;
overscroll-behavior: none; /* Предотвращает "bounce" эффект на iOS */
overflow: hidden;
will-change: transform; /* Ускорение для анимаций */
} }
#app-container { #app-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-height: 100vh; height: 100vh;
} width: 100%;
/* Global Responsive Styles */
.app {
position: relative; position: relative;
min-height: 100vh; overflow: hidden;
background-color: #f8f9fa;
} }
.app-header .navbar-brand { /* Основная область содержимого */
font-weight: bold; .app-content {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
-webkit-overflow-scrolling: touch; /* Плавная прокрутка на iOS */
width: 100%;
height: calc(100% - var(--nav-height, 60px)); /* Высота за вычетом навбара */
position: absolute;
top: 0;
left: 0;
right: 0;
} }
.app-header .btn { /* Анимация переходов между страницами */
min-width: 100px; /* Чтобы кнопки были примерно одинаковой ширины */ .page-transition-enter-active,
.page-transition-leave-active {
transition: opacity 0.2s ease, transform 0.2s ease;
} }
main.container { .page-transition-enter-from,
flex-grow: 1; .page-transition-leave-to {
padding-top: 1rem; opacity: 0;
padding-bottom: 1rem; transform: translateY(10px);
} }
/* Адаптивные глобальные стили */ /* Мобильная навигация */
.mobile-nav {
display: flex;
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: var(--nav-height, 60px);
background: white;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-decoration: none;
color: #6c757d;
transition: all 0.2s ease;
padding: 4px 0;
position: relative;
}
.nav-icon {
position: relative;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 3px;
height: 28px;
}
.nav-item i {
font-size: 1.5rem;
transition: transform 0.2s ease;
}
.nav-item span {
font-size: 0.7rem;
font-weight: 500;
}
.nav-item.active {
color: #667eea;
}
.nav-item.active i {
transform: scale(1.15);
}
/* Индикатор непрочитанных сообщений */
.unread-badge {
position: absolute;
top: -5px;
right: -8px;
background-color: #ff6b6b;
color: white;
font-size: 0.6rem;
border-radius: 50%;
min-width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
box-shadow: 0 2px 5px rgba(255, 107, 107, 0.3);
border: 1px solid white;
}
/* CSS переменные для динамической настройки */
:root {
--nav-height: 60px;
--header-height: 56px;
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0px);
}
/* Учет Safe Area на устройствах с вырезами (iPhone X и новее) */
@supports (padding-bottom: env(safe-area-inset-bottom)) {
.mobile-nav {
height: calc(var(--nav-height) + var(--safe-area-inset-bottom));
padding-bottom: var(--safe-area-inset-bottom);
}
}
/* Адаптивные настройки для различных устройств */
@media (max-width: 576px) { @media (max-width: 576px) {
.app-header .navbar-brand { :root {
font-size: 1.2rem; --nav-height: 56px;
} }
.app-header .btn { .nav-item i {
min-width: 80px; font-size: 1.3rem;
font-size: 0.9rem;
padding: 0.4rem 0.8rem;
}
.container {
padding-left: 15px;
padding-right: 15px;
} }
} }
/* Small phones */ /* Маленькие телефоны */
@media (max-width: 375px) { @media (max-width: 375px) {
.app-header .navbar-brand { .nav-item span {
font-size: 1.1rem; font-size: 0.65rem;
}
.app-header .btn {
min-width: 70px;
font-size: 0.85rem;
padding: 0.35rem 0.7rem;
} }
} }
/* Landscape orientation on mobile */ /* Ландшафтная ориентация на мобильных */
@media (max-height: 450px) and (orientation: landscape) { @media (max-height: 450px) and (orientation: landscape) {
.app-header { :root {
padding-top: 0.25rem !important; --nav-height: 50px;
padding-bottom: 0.25rem !important; }
.nav-item {
flex-direction: row;
gap: 5px;
}
.nav-icon {
margin-bottom: 0;
} }
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,8 @@
<template> <template>
<div class="modern-chat-view"> <div class="app-chat-view">
<!-- Header Section --> <!-- Фиксированный хедер -->
<div v-if="conversationData && !loadingInitialMessages" class="chat-header"> <div v-if="conversationData && !loadingInitialMessages" class="chat-header">
<div class="container">
<div class="header-content"> <div class="header-content">
<div class="chat-info">
<router-link to="/chats" class="back-button"> <router-link to="/chats" class="back-button">
<i class="bi-arrow-left"></i> <i class="bi-arrow-left"></i>
</router-link> </router-link>
@ -32,9 +30,9 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
<!-- Основное содержимое -->
<main class="chat-content">
<!-- Loading State --> <!-- Loading State -->
<div v-if="loadingInitialMessages" class="loading-section"> <div v-if="loadingInitialMessages" class="loading-section">
<div class="loading-spinner"> <div class="loading-spinner">
@ -45,7 +43,6 @@
<!-- Error State --> <!-- Error State -->
<div v-if="error" class="error-section"> <div v-if="error" class="error-section">
<div class="container">
<div class="error-card"> <div class="error-card">
<i class="bi-exclamation-triangle"></i> <i class="bi-exclamation-triangle"></i>
<h3>Ошибка загрузки</h3> <h3>Ошибка загрузки</h3>
@ -53,11 +50,9 @@
<button class="retry-btn" @click="fetchInitialMessages">Попробовать снова</button> <button class="retry-btn" @click="fetchInitialMessages">Попробовать снова</button>
</div> </div>
</div> </div>
</div>
<!-- No Data State --> <!-- No Data State -->
<div v-if="!loadingInitialMessages && !conversationData && !error" class="empty-section"> <div v-if="!loadingInitialMessages && !conversationData && !error" class="empty-section">
<div class="container">
<div class="empty-card"> <div class="empty-card">
<i class="bi-chat-dots"></i> <i class="bi-chat-dots"></i>
<h3>Диалог недоступен</h3> <h3>Диалог недоступен</h3>
@ -65,14 +60,9 @@
<router-link to="/chats" class="action-btn primary">К списку диалогов</router-link> <router-link to="/chats" class="action-btn primary">К списку диалогов</router-link>
</div> </div>
</div> </div>
</div>
<!-- Main Content -->
<div v-if="conversationData && !loadingInitialMessages && !error" class="main-content">
<div class="container">
<div class="chat-card">
<!-- Messages Container --> <!-- Messages Container -->
<div ref="messagesContainer" class="messages-container"> <div v-if="conversationData && !loadingInitialMessages && !error" ref="messagesContainer" class="messages-container">
<template v-for="item in messagesWithDateSeparators" :key="item.id"> <template v-for="item in messagesWithDateSeparators" :key="item.id">
<!-- Date Separator --> <!-- Date Separator -->
<div v-if="item.type === 'date_separator'" class="date-separator"> <div v-if="item.type === 'date_separator'" class="date-separator">
@ -125,9 +115,10 @@
<i class="bi-arrow-down"></i> <i class="bi-arrow-down"></i>
</button> </button>
</div> </div>
</main>
<!-- Message Input Area --> <!-- Фиксированный нижний блок для ввода сообщений -->
<div v-if="isAuthenticated" class="message-input-container"> <div v-if="isAuthenticated && conversationData" class="message-input-container">
<form @submit.prevent="editingMessageId ? saveEditedMessage() : sendMessage()" class="message-form"> <form @submit.prevent="editingMessageId ? saveEditedMessage() : sendMessage()" class="message-form">
<input <input
type="text" type="text"
@ -159,9 +150,6 @@
</div> </div>
</form> </form>
</div> </div>
</div>
</div>
</div>
<!-- Context Menu (Mobile) --> <!-- Context Menu (Mobile) -->
<div v-if="contextMenu && contextMenu.visible" <div v-if="contextMenu && contextMenu.visible"
@ -183,12 +171,12 @@
</template> </template>
<script setup> <script setup>
// Используем существующий script setup // Используем существующий script setup без изменений
import { ref, onMounted, onUnmounted, nextTick, computed, watch } from 'vue'; import { ref, onMounted, onUnmounted, nextTick, computed, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import api from '@/services/api'; import api from '@/services/api';
import { useAuth } from '@/auth'; import { useAuth } from '@/auth';
import { getSocket, connectSocket, disconnectSocket } from '@/services/socketService'; import { getSocket, connectSocket } from '@/services/socketService';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -691,50 +679,39 @@ const handleClickOutsideContextMenu = (event) => {
</script> </script>
<style scoped> <style scoped>
/* Global Reset and Base Styles */ /* Основные стили */
.modern-chat-view { .app-chat-view {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background-attachment: fixed; /* Фиксирует фон для растяжения на весь экран */
background-size: cover;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
width: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative; height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
width: 100%; width: 100%;
padding: 0 15px; position: relative;
background-color: #f8f9fa;
background-image: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
overflow: hidden;
} }
/* Header Section */ /* Хедер */
.chat-header { .chat-header {
background: rgba(33, 33, 60, 0.8); /* Более темный, менее прозрачный фон */ background: rgba(33, 33, 60, 0.9);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border-bottom: 1px solid rgba(255, 255, 255, 0.2); border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding: 1rem 0; padding: 0.8rem 1rem;
color: white; color: white;
position: sticky; position: sticky;
top: 0; top: 0;
z-index: 10; z-index: 10;
flex-shrink: 0;
height: var(--header-height, 56px);
} }
.header-content { .header-content {
display: flex;
align-items: center;
justify-content: space-between;
}
.chat-info {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 1rem; gap: 1rem;
max-width: 800px;
margin: 0 auto;
width: 100%;
} }
.back-button { .back-button {
@ -747,22 +724,17 @@ const handleClickOutsideContextMenu = (event) => {
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
color: white; color: white;
text-decoration: none; text-decoration: none;
transition: all 0.3s ease; transition: all 0.2s ease;
flex-shrink: 0;
} }
.back-button:hover {
background: rgba(255, 255, 255, 0.3);
transform: translateX(-2px);
}
/* Аватар участника (новый) */
.participant-avatar { .participant-avatar {
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 50%; border-radius: 50%;
overflow: hidden; overflow: hidden;
border: 2px solid white; border: 2px solid white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); flex-shrink: 0;
} }
.avatar-image { .avatar-image {
@ -786,29 +758,28 @@ const handleClickOutsideContextMenu = (event) => {
} }
.participant-info { .participant-info {
display: flex; overflow: hidden;
flex-direction: column;
} }
.participant-name { .participant-name {
margin: 0; margin: 0;
font-size: 1.2rem; font-size: 1.1rem;
font-weight: 600; font-weight: 600;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5); /* Добавлена тень для лучшей видимости текста */ white-space: nowrap;
color: #ffffff; /* Гарантирует белый цвет текста */ overflow: hidden;
text-overflow: ellipsis;
} }
.typing-status { .typing-status {
margin: 0; margin: 0;
font-size: 0.8rem; font-size: 0.8rem;
opacity: 0.8; opacity: 0.9;
font-style: italic; font-style: italic;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: 0.4rem;
} }
/* Анимация "печатает" */
.typing-animation { .typing-animation {
display: flex; display: flex;
gap: 3px; gap: 3px;
@ -837,18 +808,29 @@ const handleClickOutsideContextMenu = (event) => {
50% { transform: translateY(-4px); } 50% { transform: translateY(-4px); }
} }
/* Loading State */ /* Основная область содержимого */
.loading-section { .chat-content {
flex: 1;
overflow: hidden;
position: relative;
display: flex;
flex-direction: column;
}
/* Состояния загрузки и ошибки */
.loading-section,
.error-section,
.empty-section {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
min-height: 400px; flex: 1;
flex-grow: 1; padding: 1rem;
color: white;
} }
.loading-spinner { .loading-spinner {
text-align: center; text-align: center;
color: white;
} }
.spinner { .spinner {
@ -877,22 +859,19 @@ const handleClickOutsideContextMenu = (event) => {
100% { transform: rotate(360deg); } 100% { transform: rotate(360deg); }
} }
/* Error and Empty States */ .error-card,
.error-section, .empty-section { .empty-card {
padding: 4rem 0; background: white;
flex-grow: 1; border-radius: 16px;
} padding: 2rem;
.error-card, .empty-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
padding: 3rem;
text-align: center; text-align: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
max-width: 400px;
width: 100%;
} }
.error-card i, .empty-card i { .error-card i,
.empty-card i {
font-size: 3rem; font-size: 3rem;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
@ -905,71 +884,46 @@ const handleClickOutsideContextMenu = (event) => {
color: #6c757d; color: #6c757d;
} }
.error-card h3, .empty-card h3 { .error-card h3,
color: #495057; .empty-card h3 {
color: #343a40;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.retry-btn { .retry-btn,
.action-btn.primary {
background: linear-gradient(45deg, #667eea, #764ba2); background: linear-gradient(45deg, #667eea, #764ba2);
color: white; color: white;
border: none; border: none;
padding: 0.75rem 2rem; padding: 0.75rem 1.5rem;
border-radius: 50px; border-radius: 50px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease; transition: all 0.3s ease;
text-decoration: none;
display: inline-block;
box-shadow: 0 4px 10px rgba(102, 126, 234, 0.3);
} }
.retry-btn:hover { /* Контейнер сообщений */
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
/* Main Content */
.main-content {
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 1rem 0;
position: relative;
}
/* Chat Card (новое) */
.chat-card {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 20px;
overflow: hidden;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
height: calc(100vh - 140px);
position: relative;
margin: 0 auto; /* Центрировать карточку */
width: 100%; /* Ширина 100% в контейнере */
}
/* Messages Container */
.messages-container { .messages-container {
display: flex; flex: 1;
flex-direction: column;
gap: 1rem;
padding: 1.5rem;
overflow-y: auto; overflow-y: auto;
flex-grow: 1; padding: 1rem;
position: relative;
height: calc(100% - 80px);
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: rgba(102, 126, 234, 0.5) rgba(255, 255, 255, 0.1); scrollbar-color: rgba(102, 126, 234, 0.5) rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
gap: 0.8rem;
-webkit-overflow-scrolling: touch;
} }
.messages-container::-webkit-scrollbar { .messages-container::-webkit-scrollbar {
width: 6px; width: 5px;
} }
.messages-container::-webkit-scrollbar-track { .messages-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1); background: transparent;
} }
.messages-container::-webkit-scrollbar-thumb { .messages-container::-webkit-scrollbar-thumb {
@ -977,11 +931,11 @@ const handleClickOutsideContextMenu = (event) => {
border-radius: 6px; border-radius: 6px;
} }
/* Кнопка прокрутки вниз (новая) */ /* Кнопка прокрутки вниз */
.scroll-bottom-btn { .scroll-bottom-btn {
position: absolute; position: absolute;
right: 20px; right: 16px;
bottom: 20px; bottom: 16px;
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 50%; border-radius: 50%;
@ -993,22 +947,15 @@ const handleClickOutsideContextMenu = (event) => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease;
font-size: 1.2rem;
z-index: 5; z-index: 5;
} }
.scroll-bottom-btn:hover { /* Разделитель даты */
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.6);
}
/* Date Separator */
.date-separator { .date-separator {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 1.5rem 0; margin: 1rem 0;
} }
.date-badge { .date-badge {
@ -1016,14 +963,14 @@ const handleClickOutsideContextMenu = (event) => {
color: #495057; color: #495057;
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
border-radius: 20px; border-radius: 20px;
font-size: 0.9rem; font-size: 0.85rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
/* Message Styles */ /* Стили сообщений */
.message-item { .message-item {
display: flex; display: flex;
margin-bottom: 0.5rem; margin-bottom: 0.4rem;
} }
.message-item.sent { .message-item.sent {
@ -1032,11 +979,11 @@ const handleClickOutsideContextMenu = (event) => {
.message-wrapper { .message-wrapper {
position: relative; position: relative;
max-width: 75%; max-width: 80%;
} }
.message-bubble { .message-bubble {
padding: 0.8rem 1rem; padding: 0.7rem 1rem;
border-radius: 18px; border-radius: 18px;
position: relative; position: relative;
transition: all 0.2s ease; transition: all 0.2s ease;
@ -1046,30 +993,18 @@ const handleClickOutsideContextMenu = (event) => {
background: linear-gradient(135deg, #667eea, #764ba2); background: linear-gradient(135deg, #667eea, #764ba2);
color: white; color: white;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.2); box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);
} }
.received .message-bubble { .received .message-bubble {
background: rgba(255, 255, 255, 0.95); background: white;
color: #333; color: #333;
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
.sent .message-bubble.edited {
background: linear-gradient(135deg, #5c70d6, #6742a6);
}
.sent .message-bubble.with-actions {
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.message-content {
position: relative;
} }
.message-text { .message-text {
margin: 0 0 0.5rem; margin: 0 0 0.4rem;
line-height: 1.4; line-height: 1.4;
word-break: break-word; word-break: break-word;
} }
@ -1086,36 +1021,19 @@ const handleClickOutsideContextMenu = (event) => {
justify-content: flex-start; justify-content: flex-start;
} }
.message-time {
opacity: 0.8;
}
.message-edited {
font-style: italic;
opacity: 0.8;
}
.message-status {
opacity: 0.8;
}
.message-actions { .message-actions {
position: absolute; position: absolute;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
left: -60px;
display: flex; display: flex;
gap: 0.5rem; gap: 0.4rem;
opacity: 0; opacity: 0;
transition: opacity 0.3s ease, transform 0.3s ease; transition: opacity 0.3s ease;
} }
.sent .message-wrapper:hover .message-actions { .sent .message-wrapper:hover .message-actions {
opacity: 1; opacity: 1;
transform: translateY(-50%) scale(1);
}
.sent .message-actions {
left: -60px; /* Outside message bubble */
} }
.action-icon { .action-icon {
@ -1128,51 +1046,53 @@ const handleClickOutsideContextMenu = (event) => {
border: none; border: none;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
background: rgba(255, 255, 255, 0.95); background: white;
color: #495057; color: #495057;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
} }
.edit-icon:hover { .edit-icon:hover {
background: #0dcaf0; background: #0dcaf0;
color: white; color: white;
transform: scale(1.1);
} }
.delete-icon:hover { .delete-icon:hover {
background: #dc3545; background: #dc3545;
color: white; color: white;
transform: scale(1.1);
} }
/* Message Input Container */ /* Контейнер ввода сообщений */
.message-input-container { .message-input-container {
padding: 1rem;
background: white; background: white;
border-top: 1px solid rgba(0, 0, 0, 0.05); border-top: 1px solid rgba(0, 0, 0, 0.1);
padding: 0.8rem 1rem;
position: sticky;
bottom: 0;
z-index: 10;
max-width: 800px;
margin: 0 auto;
width: 100%;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
} }
.message-form { .message-form {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.75rem; gap: 0.75rem;
position: relative;
} }
.message-input { .message-input {
flex-grow: 1; flex: 1;
padding: 0.8rem 1rem; padding: 0.7rem 1rem;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 24px; border-radius: 24px;
font-size: 0.95rem; font-size: 0.95rem;
background: white; background: white;
transition: all 0.3s ease;
} }
.message-input:focus { .message-input:focus {
outline: none; outline: none;
border-color: #667eea; border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2); box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.15);
} }
.input-actions { .input-actions {
@ -1180,26 +1100,18 @@ const handleClickOutsideContextMenu = (event) => {
gap: 0.5rem; gap: 0.5rem;
} }
/* Action Buttons */
.action-btn { .action-btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.5rem;
padding: 0.8rem 1.25rem; padding: 0.7rem 1.2rem;
border: none; border: none;
border-radius: 50px; border-radius: 50px;
font-weight: 600; font-weight: 600;
font-size: 0.9rem; font-size: 0.9rem;
transition: all 0.3s ease;
cursor: pointer; cursor: pointer;
} }
.action-btn.primary {
background: linear-gradient(45deg, #667eea, #764ba2);
color: white;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.action-btn.secondary { .action-btn.secondary {
background: #f1f3f5; background: #f1f3f5;
color: #495057; color: #495057;
@ -1210,16 +1122,7 @@ const handleClickOutsideContextMenu = (event) => {
cursor: not-allowed; cursor: not-allowed;
} }
.action-btn.primary:not(:disabled):hover { /* Контекстное меню */
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
}
.action-btn.secondary:not(:disabled):hover {
background: #e9ecef;
}
/* Context Menu */
.context-menu { .context-menu {
position: fixed; position: fixed;
z-index: 1000; z-index: 1000;
@ -1230,11 +1133,6 @@ const handleClickOutsideContextMenu = (event) => {
min-width: 180px; min-width: 180px;
} }
.context-menu-content {
display: flex;
flex-direction: column;
}
.context-menu-item { .context-menu-item {
display: flex; display: flex;
align-items: center; align-items: center;
@ -1245,6 +1143,7 @@ const handleClickOutsideContextMenu = (event) => {
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
transition: background 0.2s; transition: background 0.2s;
width: 100%;
} }
.context-menu-item:hover { .context-menu-item:hover {
@ -1263,47 +1162,19 @@ const handleClickOutsideContextMenu = (event) => {
color: #dc3545; color: #dc3545;
} }
/* Responsive adjustments */ /* Адаптивные стили */
@media (max-width: 576px) { @media (max-width: 576px) {
.chat-card {
height: calc(100vh - 120px);
margin: 0;
border-radius: 0;
}
.message-wrapper { .message-wrapper {
max-width: 85%; max-width: 85%;
} }
.message-actions { .message-actions {
display: none; /* Hide on mobile, use context menu instead */ display: none; /* Скрываем на мобильных, используем контекстное меню */
} }
.messages-container { .action-btn {
padding: 1rem; padding: 0.6rem 1rem;
} font-size: 0.85rem;
.chat-input {
padding: 0.8rem;
}
.message-bubble {
padding: 0.7rem 0.9rem;
}
.message-meta {
font-size: 0.65rem;
}
.scroll-bottom-btn {
width: 36px;
height: 36px;
right: 15px;
bottom: 15px;
}
.participant-name {
font-size: 1.1rem;
} }
.back-button { .back-button {
@ -1312,97 +1183,61 @@ const handleClickOutsideContextMenu = (event) => {
} }
} }
/* Small phones */
@media (max-width: 375px) { @media (max-width: 375px) {
.message-wrapper { .message-wrapper {
max-width: 90%; max-width: 90%;
} }
.chat-input {
padding: 0.6rem;
}
.message-text {
font-size: 0.95rem;
}
.message-typing {
font-size: 0.85rem;
}
.context-menu { .context-menu {
width: 160px; width: 160px;
} }
}
.chat-info { /* Учитываем наличие нижней навигационной панели */
gap: 0.5rem; @media (max-height: 680px) {
.messages-container {
padding: 0.8rem;
gap: 0.6rem;
}
.message-input-container {
padding: 0.7rem;
}
.message-input {
padding: 0.6rem 0.8rem;
} }
.action-btn { .action-btn {
padding: 0.6rem 1rem; padding: 0.6rem 1rem;
font-size: 0.85rem;
} }
} }
/* Large phones and small tablets */ /* Ландшафтная ориентация на мобильных */
@media (min-width: 577px) and (max-width: 767px) { @media (max-height: 450px) and (orientation: landscape) {
.chat-card {
height: calc(100vh - 130px);
}
.message-wrapper {
max-width: 80%;
}
.message-actions {
opacity: 0.7; /* Always visible but slightly transparent */
}
}
/* Tablets */
@media (min-width: 768px) and (max-width: 991px) {
.container {
max-width: 700px;
}
.chat-card {
height: calc(100vh - 140px);
}
}
/* Small desktops */
@media (min-width: 992px) and (max-width: 1199px) {
.container {
max-width: 800px;
}
}
/* Landscape orientation on mobile */
@media (max-height: 500px) and (orientation: landscape) {
.chat-header { .chat-header {
padding: 0.5rem 0; padding: 0.5rem 1rem;
height: 46px;
} }
.chat-card { .participant-avatar {
height: calc(100vh - 80px); width: 32px;
height: 32px;
} }
.messages-container { .participant-name {
padding: 0.8rem; font-size: 1rem;
height: calc(100% - 60px);
} }
.chat-input-container { .message-input-container {
padding: 0.5rem; padding: 0.5rem;
min-height: 60px;
} }
}
.message-item { /* Учет нижней мобильной навигации (примерно 60px) */
margin-bottom: 0.4rem; @supports (padding-bottom: env(safe-area-inset-bottom)) {
} .message-input-container {
padding-bottom: calc(0.8rem + env(safe-area-inset-bottom, 0px));
.date-separator {
margin: 0.8rem 0;
} }
} }
</style> </style>

File diff suppressed because it is too large Load Diff