еще стили

This commit is contained in:
Professional 2025-05-22 23:20:11 +07:00
parent 55da747051
commit 5f5d31d0b5
4 changed files with 2344 additions and 862 deletions

View File

@ -1,39 +1,47 @@
<template>
<div id="app-container">
<header class="app-header fixed-top shadow-sm">
<nav class="container navbar navbar-expand-lg navbar-dark">
<router-link to="/" class="navbar-brand fs-4">DatingApp</router-link>
<header class="app-header fixed-top">
<nav class="container navbar navbar-expand-lg">
<router-link to="/" class="navbar-brand">
<span class="brand-text">Dating<span class="highlight">App</span></span>
</router-link>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto align-items-center">
<template v-if="authState">
<li class="nav-item me-3 user-greeting">
<span class="nav-link">Привет, {{ userData?.name || 'Пользователь' }}!</span>
</li>
<li class="nav-item">
<router-link to="/swipe" class="nav-link" active-class="active">Поиск</router-link>
</li>
<li class="nav-item">
<router-link to="/chats" class="nav-link" active-class="active">Чаты</router-link>
</li>
<li class="nav-item">
<router-link to="/profile" class="nav-link" active-class="active">Профиль</router-link>
</li>
<li class="nav-item">
<button @click="handleLogout" class="btn btn-outline-primary ms-2">Выход</button>
</li>
</template>
<template v-else>
<li class="nav-item">
<router-link to="/login" class="nav-link" active-class="active">Войти</router-link>
</li>
<li class="nav-item">
<router-link to="/register" class="btn btn-primary ms-2">Регистрация</router-link>
</li>
</template>
</ul>
<div v-if="authState" class="navbar-nav ms-auto align-items-center">
<div class="nav-item user-greeting">
<div class="user-avatar">
<img v-if="userData?.photoURL" :src="userData.photoURL" alt="Фото профиля" />
<div v-else class="avatar-placeholder">
{{ userData?.name?.charAt(0) || 'П' }}
</div>
</div>
<span class="greeting">Привет, {{ userData?.name || 'Пользователь' }}!</span>
</div>
<div class="nav-links">
<router-link to="/swipe" class="nav-link" active-class="active">
<i class="bi bi-search-heart"></i>
<span class="nav-text">Поиск</span>
</router-link>
<router-link to="/chats" class="nav-link" active-class="active">
<i class="bi bi-chat-heart"></i>
<span class="nav-text">Чаты</span>
</router-link>
<router-link to="/profile" class="nav-link" active-class="active">
<i class="bi bi-person"></i>
<span class="nav-text">Профиль</span>
</router-link>
<button @click="handleLogout" class="logout-btn">
<i class="bi bi-box-arrow-right"></i>
<span class="btn-text">Выход</span>
</button>
</div>
</div>
<div v-else class="navbar-nav ms-auto align-items-center">
<router-link to="/login" class="nav-link login-link" active-class="active">Войти</router-link>
<router-link to="/register" class="register-btn">Регистрация</router-link>
</div>
</div>
</nav>
</header>
@ -42,10 +50,16 @@
<router-view />
</main>
<footer class="app-footer mt-auto py-3 text-center">
<footer class="app-footer mt-auto py-4 text-center">
<div class="container">
<!-- Footer text color will be handled by global styles -->
<span>© {{ new Date().getFullYear() }} Dating App PWA</span>
<div class="footer-content">
<span class="copyright">© {{ new Date().getFullYear() }} Dating App PWA</span>
<div class="social-links">
<a href="#" class="social-link"><i class="bi bi-instagram"></i></a>
<a href="#" class="social-link"><i class="bi bi-twitter-x"></i></a>
<a href="#" class="social-link"><i class="bi bi-facebook"></i></a>
</div>
</div>
</div>
</footer>
</div>
@ -101,64 +115,96 @@ const handleLogout = async () => {
/* Глобальные стили */
@import 'bootstrap/dist/css/bootstrap.min.css';
@import url('https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css');
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap');
:root {
--primary-color: #E91E63; /* Яркий розовый */
--primary-hover-color: #C2185B; /* Более темный розовый для ховера */
/* Основная цветовая палитра 2025 */
--primary-color: #FF3A8C; /* Яркий розовый, новый оттенок */
--primary-hover-color: #E42A78; /* Более темный розовый для hover */
--primary-gradient: linear-gradient(135deg, #FF3A8C 0%, #FF7676 100%); /* Модный градиент */
--background-color: #121212; /* Глубокий темно-серый (почти черный) */
--card-bg-color: #1E1E1E; /* Темно-серый для карточек */
--text-color: #E0E0E0; /* Светло-серый текст */
--text-secondary-color: #BDBDBD; /* Менее контрастный текст */
/* Темная тема 2.0 с улучшенными контрастами */
--background-color: #0a0a0f; /* Очень темный, почти черный с легким синим оттенком */
--card-bg-color: #16161f; /* Темно-синий для карточек */
--card-bg-gradient: linear-gradient(135deg, #16161f 0%, #1e1e2a 100%); /* Стильный градиент для карточек */
--hover-bg-color: #20202c; /* Чуть светлее для hover эффектов */
--border-color: var(--primary-color); /* Розовый для активных границ */
--border-subtle-color: #333333; /* Тонкая темная граница для разделения */
--input-bg-color: #2C2C2C; /* Фон для инпутов */
/* Текстовые цвета с улучшенным контрастом */
--text-color: #f0f0f5; /* Почти белый с легким синим оттенком для основного текста */
--text-secondary-color: #a8a8c0; /* Серебристо-синий для вторичного текста */
--text-disabled-color: #5a5a6e; /* Более темный для неактивного текста */
/* Границы и разделители */
--border-color: rgba(255, 58, 140, 0.3); /* Полупрозрачный розовый для активных границ */
--border-subtle-color: #27273a; /* Слабовидимая граница для разделения */
--border-radius-sm: 0.5rem; /* Маленькое скругление */
--border-radius: 1rem; /* Стандартное скругление */
--border-radius-lg: 1.5rem; /* Большое скругление */
/* Поля ввода */
--input-bg-color: #1c1c26; /* Темно-синий фон для инпутов */
--input-bg-color-disabled: #13131c; /* Еще темнее для дизактированных полей */
--input-text-color: var(--text-color);
--input-border-color: #444444; /* Граница для инпутов */
--input-border-color: #30304d; /* Синеватая граница для инпутов */
--input-focus-border-color: var(--primary-color);
--input-placeholder-color: #757575;
--button-primary-bg: var(--primary-color);
--button-primary-text: #ffffff; /* Белый текст на яркой кнопке */
--button-primary-hover-bg: var(--primary-hover-color);
--button-secondary-bg: #333333;
--button-secondary-text: var(--text-color);
--button-secondary-hover-bg: #444444;
--input-placeholder-color: #52526e; /* Серо-синий для плейсхолдеров */
--link-color: var(--primary-color);
--link-hover-color: var(--primary-hover-color);
--success-bg: #1E402A;
--success-text: #D1E7DD;
--success-border: #BADBCC;
--danger-bg: #4D2227;
--danger-text: #F8D7DA;
--danger-border: #F5C2C7;
--warning-bg: #594402;
--warning-text: #FFF3CD;
--warning-border: #FFEEBA;
--info-bg: #1C3E4E;
--info-text: #CFF4FC;
--info-border: #B6EFFB;
/* Кнопки */
--button-primary-bg: var(--primary-gradient); /* Градиент вместо сплошного цвета */
--button-primary-text: #ffffff; /* Белый текст на ярких кнопках */
--button-primary-hover-bg: linear-gradient(135deg, #E42A78 0%, #E05B5B 100%); /* Более темный градиент для hover */
--button-primary-shadow: 0 4px 15px rgba(255, 58, 140, 0.4); /* Стильная тень с цветом кнопки */
--button-secondary-bg: rgba(255, 255, 255, 0.1); /* Полупрозрачный фон */
--button-secondary-text: var(--text-color);
--button-secondary-hover-bg: rgba(255, 255, 255, 0.15); /* Чуть светлее при наведении */
--button-secondary-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
/* Ссылки */
--link-color: #FF7AC6; /* Светло-розовый для ссылок */
--link-hover-color: #FFB5E8; /* Еще светлее при наведении */
/* Эффекты */
--box-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.2);
--box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
--box-shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.5);
--neomorphic-shadow: -5px -5px 15px rgba(28, 28, 40, 0.5), 5px 5px 15px rgba(0, 0, 0, 0.5);
/* Анимация */
--transition-fast: 0.15s;
--transition-normal: 0.25s;
--transition-slow: 0.4s;
/* Цвета оповещений */
--success-bg: #102b18;
--success-text: #d1fadc;
--success-border: #1e5c34;
--danger-bg: #350f1a;
--danger-text: #ffc8d3;
--danger-border: #69172f;
--warning-bg: #3a2900;
--warning-text: #ffdea8;
--warning-border: #705618;
--info-bg: #0a2a3d;
--info-text: #b6e7ff;
--info-border: #144b72;
}
html, body {
height: 100%;
font-family: 'Montserrat', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
body {
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";
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
padding-top: 70px; /* Компенсация высоты фиксированной навигационной панели */
letter-spacing: 0.01em;
}
#app-container {
@ -177,56 +223,222 @@ body {
max-width: 1200px;
}
/* Стили для Navbar */
/* Стили для Navbar - новая версия 2025 */
.app-header {
background-color: rgba(18, 18, 18, 0.85); /* --background-color с прозрачностью */
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--border-subtle-color); /* Тонкая граница */
background-color: rgba(10, 10, 15, 0.85);
backdrop-filter: blur(15px);
border-bottom: 1px solid rgba(255, 58, 140, 0.15);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
padding: 0.5rem 0;
}
.navbar-brand {
color: var(--primary-color) !important;
font-weight: bold;
display: flex;
align-items: center;
}
.navbar-brand:hover {
color: var(--link-hover-color) !important;
.brand-text {
font-size: 1.5rem;
font-weight: 700;
background: linear-gradient(135deg, #f0f0f5 0%, #a8a8c0 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: 0.02em;
}
.brand-text .highlight {
background: var(--primary-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Пользовательское меню */
.user-greeting {
display: flex;
align-items: center;
gap: 0.75rem;
margin-right: 1.5rem;
padding: 0.5rem 0;
}
.user-avatar {
width: 38px;
height: 38px;
border-radius: 50%;
overflow: hidden;
position: relative;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
.user-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--primary-gradient);
color: white;
font-weight: 600;
font-size: 1.1rem;
}
.greeting {
display: inline-block;
font-weight: 500;
color: var(--text-color);
}
/* Навигационные ссылки */
.nav-links {
display: flex;
align-items: center;
gap: 0.5rem;
}
.nav-link {
color: var(--text-secondary-color) !important;
padding-left: 0.75rem;
padding-right: 0.75rem;
transition: color 0.2s ease-in-out;
}
.nav-link:hover,
.nav-link.active {
color: var(--primary-color) !important;
}
.user-greeting .nav-link {
color: var(--text-secondary-color) !important;
cursor: default;
}
.user-greeting .nav-link:hover {
color: var(--text-secondary-color) !important;
}
.navbar-toggler {
border-color: rgba(233, 30, 99, 0.5); /* var(--primary-color) с прозрачностью */
}
.navbar-toggler-icon {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba(233, 30, 99, 0.8)' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");
}
/* Footer */
.app-footer {
border-top: 1px solid var(--border-subtle-color);
background-color: var(--background-color); /* Явно задаем фон, если он отличается от body */
}
.app-footer span {
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-secondary-color);
padding: 0.5rem 1rem;
border-radius: var(--border-radius-sm);
transition: all var(--transition-normal) ease;
position: relative;
overflow: hidden;
}
.nav-link i {
font-size: 1.2rem;
}
.nav-link::before {
content: '';
position: absolute;
bottom: 0;
left: 50%;
width: 0;
height: 3px;
background: var(--primary-gradient);
transform: translateX(-50%);
border-radius: 3px;
transition: width var(--transition-normal) ease;
}
.nav-link:hover {
color: var(--text-color) !important;
background-color: rgba(255, 255, 255, 0.05);
}
.nav-link.active {
color: var(--text-color) !important;
background-color: rgba(255, 58, 140, 0.1);
}
.nav-link.active::before {
width: 80%;
}
/* Кнопка выхода */
.logout-btn {
display: flex;
align-items: center;
gap: 0.5rem;
background: transparent;
color: var(--text-secondary-color);
border: 1px solid var(--border-subtle-color);
border-radius: var(--border-radius-sm);
padding: 0.5rem 1rem;
transition: all var(--transition-normal) ease;
margin-left: 0.75rem;
}
.logout-btn:hover {
background-color: rgba(255, 255, 255, 0.05);
color: var(--text-color);
border-color: var(--border-color);
}
/* Ссылка для регистрации */
.register-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1.25rem;
background: var(--primary-gradient);
color: var(--button-primary-text);
border: none;
border-radius: var(--border-radius-sm);
font-weight: 500;
text-decoration: none;
transition: all var(--transition-normal) ease;
box-shadow: var(--button-primary-shadow);
margin-left: 0.75rem;
}
.register-btn:hover {
background: var(--button-primary-hover-bg);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(255, 58, 140, 0.35);
color: var(--button-primary-text);
}
.login-link {
font-weight: 500;
padding: 0.5rem 1.25rem;
}
/* Стили для футера */
.app-footer {
background-color: rgba(10, 10, 15, 0.95);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(255, 58, 140, 0.15);
padding: 1.5rem 0;
}
.footer-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.copyright {
color: var(--text-secondary-color);
font-size: 0.9rem;
}
.social-links {
display: flex;
gap: 1.25rem;
margin-top: 0.5rem;
}
.social-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.05);
color: var(--text-secondary-color);
font-size: 1.2rem;
transition: all var(--transition-normal) ease;
box-shadow: var(--box-shadow-sm);
}
.social-link:hover {
background: var(--primary-gradient);
color: white;
transform: translateY(-3px);
box-shadow: var(--button-primary-shadow);
}
/* Общие стили для карточек */
.card {

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +1,60 @@
<template>
<div class="chat-view-container d-flex flex-column">
<div v-if="loadingInitialMessages" class="flex-grow-1 d-flex justify-content-center align-items-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Загрузка сообщений...</span>
<div class="spinner-container">
<div class="spinner-border spinner-primary" role="status">
<span class="visually-hidden">Загрузка сообщений...</span>
</div>
</div>
</div>
<div v-if="error" class="alert alert-danger m-3">{{ error }}</div>
<div v-if="!loadingInitialMessages && !conversationData" class="alert alert-warning m-3">
Не удалось загрузить данные диалога.
<div v-if="error" class="alert-wrapper">
<div class="custom-alert error-alert">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
{{ error }}
</div>
</div>
<div v-if="conversationData && !loadingInitialMessages" class="chat-header p-3">
<h5 class="mb-0">Чат с {{ getOtherParticipantName() || 'Собеседник' }}</h5>
<div v-if="!loadingInitialMessages && !conversationData" class="alert-wrapper">
<div class="custom-alert warning-alert">
<i class="bi bi-question-circle-fill me-2"></i>
Не удалось загрузить данные диалога.
</div>
</div>
<div ref="messagesContainer" class="messages-area flex-grow-1 p-3">
<div v-if="conversationData && !loadingInitialMessages" class="chat-header">
<div class="user-info">
<div class="user-avatar">
<img v-if="getOtherParticipant()?.photoURL" :src="getOtherParticipant().photoURL" alt="Аватар пользователя" />
<div v-else class="avatar-placeholder">
{{ getInitials(getOtherParticipantName()) }}
</div>
</div>
<div class="user-details">
<h5 class="user-name">{{ getOtherParticipantName() || 'Собеседник' }}</h5>
<span v-if="otherUserIsTyping" class="typing-indicator">
<span class="typing-dot"></span>
<span class="typing-dot"></span>
<span class="typing-dot"></span>
</span>
<span v-else class="user-status">онлайн</span>
</div>
</div>
<div class="header-actions">
<button class="btn-icon" title="Поиск в чате">
<i class="bi bi-search"></i>
</button>
<button class="btn-icon" title="Дополнительные действия">
<i class="bi bi-three-dots-vertical"></i>
</button>
</div>
</div>
<div ref="messagesContainer" class="messages-area flex-grow-1">
<template v-for="item in messagesWithDateSeparators" :key="item.id">
<div v-if="item.type === 'date_separator'" class="date-separator text-center my-2">
<span class="badge">{{ formatDateHeader(item.timestamp) }}</span>
<div v-if="item.type === 'date_separator'" class="date-separator">
<div class="date-line"></div>
<span class="date-badge">{{ formatDateHeader(item.timestamp) }}</span>
<div class="date-line"></div>
</div>
<div v-else-if="item.type === 'message'"
:class="['message-item-wrapper', item.data.sender?._id === currentUser?._id ? 'sent-wrapper' : 'received-wrapper']"
@ -26,34 +62,50 @@
@mouseleave="!isTouchDevice.value && handleMouseLeave()"
>
<div class="avatar-space" v-if="item.data.sender?._id !== currentUser?._id">
<div class="message-avatar" v-if="shouldShowAvatar(item.data, item.index)">
<img v-if="getOtherParticipant()?.photoURL" :src="getOtherParticipant().photoURL" alt="Аватар пользователя" />
<div v-else class="avatar-placeholder small">
{{ getInitials(getOtherParticipantName()) }}
</div>
</div>
</div>
<div :class="['message-bubble', item.data.sender?._id === currentUser?._id ? 'sent' : 'received']"
@click.stop="handleMessageTap(item.data, $event)"
@contextmenu.stop="handleNativeContextMenu($event, item.data)">
<div class="message-content">
<p class="mb-0">{{ item.data.text }}</p>
<small class="message-timestamp">
{{ formatMessageTimestamp(item.data.createdAt) }}
<span v-if="item.data.isEdited" class="ms-1 fst-italic">(изменено)</span>
<span v-if="item.data.sender?._id === currentUser?._id && item.data.isSending" class="ms-1 fst-italic">(отправка...)</span>
<span v-if="item.data.sender?._id === currentUser?._id && !item.data.isSending" class="ms-1">
<div class="message-info">
<span class="message-timestamp">
{{ formatMessageTimestamp(item.data.createdAt) }}
</span>
<span v-if="item.data.isEdited" class="message-status edited">
<i class="bi bi-pencil-fill"></i>
</span>
<span v-if="item.data.sender?._id === currentUser?._id && item.data.isSending" class="message-status sending">
<i class="bi bi-clock"></i>
</span>
<span v-if="item.data.sender?._id === currentUser?._id && !item.data.isSending" class="message-status">
<i :class="getMessageStatusIcon(item.data)"></i>
</span>
</small>
</div>
</div>
<!-- Кнопки действий при наведении для десктопа -->
<div v-if="!isTouchDevice.value && hoveredMessageId === item.data._id && item.data.sender?._id === currentUser?._id && !editingMessageId"
class="message-actions">
<button @click="startEditMessage(item.data._id)"
class="action-btn edit-btn"
title="Редактировать">
<i class="bi bi-pencil-fill"></i>
</button>
<button @click="confirmDeleteMessage(item.data._id)"
class="action-btn delete-btn"
title="Удалить">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<!-- Кнопка удаления для ПК (появляется при наведении) -->
<button v-if="!isTouchDevice.value && hoveredMessageId === item.data._id && item.data.sender?._id === currentUser?._id && !editingMessageId"
@click="confirmDeleteMessage(item.data._id)"
class="btn btn-sm btn-icon delete-message-btn"
title="Удалить сообщение">
<i class="bi bi-trash"></i>
</button>
<button v-if="!isTouchDevice.value && hoveredMessageId === item.data._id && item.data.sender?._id === currentUser?._id && !editingMessageId"
@click="startEditMessage(item.data._id)"
class="btn btn-sm btn-icon edit-message-btn"
title="Редактировать сообщение">
<i class="bi bi-pencil-fill"></i>
</button>
<div class="avatar-space" v-if="item.data.sender?._id === currentUser?._id"></div>
</div>
</template>
</div>
@ -61,53 +113,74 @@
<!-- Контекстное меню для мобильных устройств -->
<div v-if="contextMenu && contextMenu.visible"
ref="contextMenuRef"
class="context-menu"
:style="{ top: contextMenu.y + 'px', left: contextMenu.x + 'px' }">
<ul>
<li @click="startEditMessage(contextMenu.messageId)">
<i class="bi bi-pencil-fill"></i>
<span>Редактировать</span>
</li>
<li @click="confirmDeleteMessage(contextMenu.messageId)">
<i class="bi bi-trash"></i>
<span>Удалить сообщение</span>
</li>
</ul>
class="context-menu">
<div class="context-menu-content">
<div class="context-menu-header">
<span>Действия</span>
<button class="close-btn" @click="contextMenu.visible = false">
<i class="bi bi-x"></i>
</button>
</div>
<div class="context-menu-options">
<button @click="startEditMessage(contextMenu.messageId)" class="context-menu-option">
<i class="bi bi-pencil-fill"></i>
<span>Редактировать</span>
</button>
<button @click="confirmDeleteMessage(contextMenu.messageId)" class="context-menu-option">
<i class="bi bi-trash"></i>
<span>Удалить</span>
</button>
<button @click="contextMenu.visible = false" class="context-menu-option">
<i class="bi bi-clipboard"></i>
<span>Копировать</span>
</button>
</div>
</div>
</div>
<!-- Индикатор "печатает" -->
<div v-if="otherUserIsTyping" class="typing-indicator px-3 pb-2 fst-italic">
{{ getOtherParticipantName() || 'Собеседник' }} печатает...
<!-- Индикатор "печатает" для мобильных (дублирует функциональность верхнего индикатора) -->
<div v-if="otherUserIsTyping && isTouchDevice.value" class="mobile-typing-indicator">
{{ getOtherParticipantName() || 'Собеседник' }} печатает
<span class="typing-dots">
<span class="typing-dot"></span>
<span class="typing-dot"></span>
<span class="typing-dot"></span>
</span>
</div>
<div v-if="isAuthenticated" class="message-input-area p-3">
<form @submit.prevent="editingMessageId ? saveEditedMessage() : sendMessage()" class="d-flex">
<input
type="text"
v-model="newMessageText"
class="form-control me-2"
:placeholder="editingMessageId ? 'Редактирование сообщения...' : 'Введите сообщение...'"
:disabled="sendingMessage || savingEdit"
ref="messageInputRef"
/>
<button
type="submit"
class="btn btn-primary"
:disabled="sendingMessage || savingEdit || !newMessageText.trim()"
>
<span v-if="sendingMessage || savingEdit" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
{{ editingMessageId ? 'Сохранить' : 'Отправить' }}
<div v-if="isAuthenticated" class="message-input-area">
<div class="message-composer">
<button class="attachment-btn" title="Прикрепить файл">
<i class="bi bi-paperclip"></i>
</button>
<button
v-if="editingMessageId"
type="button"
class="btn btn-secondary ms-2"
@click="cancelEditMessage"
:disabled="savingEdit"
>
Отмена
<form @submit.prevent="editingMessageId ? saveEditedMessage() : sendMessage()" class="message-form">
<div class="message-input-wrapper">
<input
type="text"
v-model="newMessageText"
class="message-input"
:placeholder="editingMessageId ? 'Редактирование сообщения...' : 'Введите сообщение...'"
:disabled="sendingMessage || savingEdit"
ref="messageInputRef"
/>
<button type="button" class="emoji-btn" title="Эмодзи">
<i class="bi bi-emoji-smile"></i>
</button>
</div>
<button
type="submit"
class="send-button"
:disabled="sendingMessage || savingEdit || !newMessageText.trim()"
:title="editingMessageId ? 'Сохранить изменения' : 'Отправить сообщение'"
>
<span v-if="sendingMessage || savingEdit" class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<i v-else :class="editingMessageId ? 'bi bi-check2' : 'bi bi-send'"></i>
</button>
</form>
<button v-if="editingMessageId" type="button" class="cancel-edit-btn" @click="cancelEditMessage" :disabled="savingEdit">
<i class="bi bi-x-lg"></i>
</button>
</form>
</div>
</div>
</div>
</template>
@ -667,6 +740,102 @@ const handleClickOutsideContextMenu = (event) => {
background-color: var(--card-bg-color);
border-bottom: 1px solid var(--border-color); /* Using global border color */
color: var(--primary-color); /* Accent color for header text */
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 0.75rem;
}
.user-avatar {
position: relative;
width: 40px;
height: 40px;
flex-shrink: 0;
}
.user-avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.avatar-placeholder {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: var(--primary-color);
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
color: var(--button-primary-text);
}
.user-details {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.user-name {
font-size: 1rem;
font-weight: 500;
margin: 0;
}
.user-status {
font-size: 0.875rem;
color: var(--text-secondary-color);
}
.typing-indicator {
display: inline-flex;
align-items: center;
gap: 0.2rem;
font-size: 0.875rem;
color: var(--text-secondary-color);
}
.typing-dot {
width: 6px;
height: 6px;
background-color: var(--primary-color);
border-radius: 50%;
animation: typing-dot 1s infinite alternate;
}
@keyframes typing-dot {
0% {
transform: translateY(0);
}
100% {
transform: translateY(-4px);
}
}
.header-actions {
display: flex;
gap: 0.5rem;
}
.btn-icon {
background-color: transparent;
border: none;
color: var(--text-secondary-color);
padding: 0.25rem 0.5rem;
transition: color 0.2s ease-in-out;
cursor: pointer;
}
.btn-icon:hover {
color: var(--primary-color);
}
.messages-area {
@ -676,6 +845,7 @@ const handleClickOutsideContextMenu = (event) => {
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--primary-color) var(--input-bg-color);
padding: 0 1rem;
}
.messages-area::-webkit-scrollbar {
@ -730,6 +900,14 @@ const handleClickOutsideContextMenu = (event) => {
margin-bottom: 0.25rem;
}
.message-info {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: var(--text-secondary-color);
}
.message-timestamp {
font-size: 0.75em;
display: block;
@ -759,34 +937,79 @@ const handleClickOutsideContextMenu = (event) => {
.message-input-area {
background-color: var(--card-bg-color);
border-top: 1px solid var(--border-color);
padding: 0.75rem 1rem;
}
/* Input field styling is mostly handled by global App.vue styles for .form-control */
.message-composer {
display: flex;
align-items: center;
gap: 0.5rem;
}
.date-separator span.badge {
background-color: var(--card-bg-color);
color: var(--text-secondary-color);
font-weight: normal;
padding: 0.3em 0.8em;
border-radius: 12px;
.message-input-wrapper {
flex-grow: 1;
position: relative;
}
.message-input {
width: 100%;
padding: 0.6rem 2.5rem 0.6rem 1rem;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
background-color: var(--input-bg-color);
color: var(--text-color);
font-size: 0.875rem;
}
/* Buttons for delete/edit on hover */
.btn-icon {
.message-input:disabled {
background-color: var(--input-bg-color-disabled);
color: var(--text-disabled-color);
}
.attachment-btn,
.emoji-btn {
background-color: var(--primary-color);
color: var(--button-primary-text);
border: none;
border-radius: 0.5rem;
padding: 0.4rem;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
.attachment-btn:hover,
.emoji-btn:hover {
background-color: var(--primary-color-dark);
}
.send-button {
background-color: var(--primary-color);
color: var(--button-primary-text);
border: none;
border-radius: 0.5rem;
padding: 0.6rem 1.2rem;
font-size: 0.875rem;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
.send-button:disabled {
background-color: var(--primary-color-disabled);
cursor: not-allowed;
}
.cancel-edit-btn {
background-color: transparent;
border: none;
color: var(--text-secondary-color);
padding: 0.25rem 0.5rem;
opacity: 0.7;
transition: opacity 0.2s ease-in-out, color 0.2s ease-in-out;
cursor: pointer;
padding: 0.4rem;
border-radius: 0.5rem;
transition: background-color 0.2s ease-in-out;
}
.btn-icon:hover {
opacity: 1;
color: var(--primary-color);
}
.delete-message-btn:hover {
color: var(--bs-danger); /* Bootstrap danger for delete */
.cancel-edit-btn:hover {
background-color: var(--hover-bg-color);
}
/* Context Menu */
@ -801,10 +1024,51 @@ const handleClickOutsideContextMenu = (event) => {
min-width: 180px;
}
.context-menu ul {
list-style: none;
padding: 0;
margin: 0;
.context-menu-content {
display: flex;
flex-direction: column;
}
.context-menu-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 1rem;
background-color: var(--primary-color);
color: var(--button-primary-text);
border-top-left-radius: 8px;
border-top-right-radius: 8px;
}
.close-btn {
background: transparent;
border: none;
color: inherit;
cursor: pointer;
font-size: 1.25rem;
}
.context-menu-options {
display: flex;
flex-direction: column;
padding: 0.5rem 1rem;
gap: 0.5rem;
}
.context-menu-option {
background: transparent;
border: none;
color: var(--text-color);
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.9rem;
cursor: pointer;
transition: background-color 0.2s ease-in-out;
}
.context-menu-option:hover {
background-color: var(--hover-bg-color);
}
.context-menu li {
@ -832,4 +1096,25 @@ const handleClickOutsideContextMenu = (event) => {
text-align: center;
}
.date-separator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 0;
}
.date-line {
flex-grow: 1;
height: 1px;
background: var(--border-color);
}
.date-badge {
background-color: var(--card-bg-color);
color: var(--text-secondary-color);
font-weight: normal;
padding: 0.3em 0.8em;
border-radius: 12px;
border: 1px solid var(--border-color);
}
</style>

File diff suppressed because it is too large Load Diff