mirror of
https://github.com/lorsanstand/Aether.git
synced 2026-06-19 12:05:16 +03:00
mobile optimization
This commit is contained in:
@@ -0,0 +1,186 @@
|
|||||||
|
# Оптимизация UI для мобильных устройств - Aether Messenger
|
||||||
|
|
||||||
|
Проект был полностью адаптирован для максимально нативного отображения на мобильных устройствах. Ниже перечислены все внесенные изменения.
|
||||||
|
|
||||||
|
## 🔧 Основные улучшения
|
||||||
|
|
||||||
|
### 1. **HTML метаданные** (`index.html`)
|
||||||
|
- ✅ Добавлена правильная viewport конфигурация (`viewport-fit=cover` для iPhone с notch)
|
||||||
|
- ✅ Установлена `theme-color` для браузерной полосы
|
||||||
|
- ✅ Включена поддержка PWA (`apple-mobile-web-app-capable`)
|
||||||
|
- ✅ Добавлена apple-touch-icon для иконки на домашнем экране
|
||||||
|
- ✅ Отключен автоматический зум при фокусе на input
|
||||||
|
- ✅ Добавлены все необходимые метаданные для мобильных браузеров
|
||||||
|
|
||||||
|
### 2. **Tailwind CSS конфигурация** (`tailwind.config.js`)
|
||||||
|
- ✅ Добавлена поддержка safe-area-inset для notch на iPhone
|
||||||
|
- ✅ Новые утилиты для touch-friendly размеров (44x44 минимум по Apple HIG)
|
||||||
|
- ✅ Добавлены медиа-запросы для обнаружения touch устройств
|
||||||
|
- ✅ Расширенная палитра спейсинга и размеров
|
||||||
|
|
||||||
|
### 3. **Глобальные стили** (`index.css`)
|
||||||
|
- ✅ Оптимизировано позиционирование root элемента (fixed для предотвращения скроллинга)
|
||||||
|
- ✅ Правильная обработка viewport height с поддержкой появления/исчезновения клавиатуры
|
||||||
|
- ✅ Отключены синие tap highlights на iOS/Android
|
||||||
|
- ✅ Улучшена поддержка прокрутки с `-webkit-overflow-scrolling: touch`
|
||||||
|
- ✅ Добавлена поддержка prefers-reduced-motion для доступности
|
||||||
|
- ✅ Оптимизирована работа с input полями (предотвращение зума на iOS)
|
||||||
|
|
||||||
|
### 4. **ChatPage оптимизация** (`src/pages/ChatPage.tsx`)
|
||||||
|
|
||||||
|
#### Отступы и размеры
|
||||||
|
- Уменьшены отступы на мобильных устройствах (p-2.5 вместо p-4)
|
||||||
|
- Оптимизированы размеры аватаров (w-10 вместо w-12 на мобиле)
|
||||||
|
- Правильные размеры иконок для мобилы (size-16 на мобиле, size-20 на десктопе)
|
||||||
|
|
||||||
|
#### Сообщения
|
||||||
|
- Уменьшена максимальная ширина пузырей сообщений (max-w-xs вместо max-w-lg на мобиле)
|
||||||
|
- Оптимизирован размер шрифта (text-sm вместо text-[15px])
|
||||||
|
- Добавлены правильные border-radius для мобилы
|
||||||
|
|
||||||
|
#### Поле ввода
|
||||||
|
- Установлена `font-size: 16px` для предотвращения iOS зума
|
||||||
|
- Увеличена высота кнопки отправления (min-h-touch)
|
||||||
|
- Оптимизированы паддинги для удобного нажатия на мобиле
|
||||||
|
|
||||||
|
#### Модальные окна
|
||||||
|
- Переход на полноэкранное отображение снизу на мобилах
|
||||||
|
- Улучшенная анимация (slide up вместо scale)
|
||||||
|
- Оптимизированы отступы и размеры шрифтов
|
||||||
|
|
||||||
|
#### Сайдбар
|
||||||
|
- Использование w-screen вместо w-full для полноэкранного отображения
|
||||||
|
- Скрытие на мобилах при выборе чата
|
||||||
|
- Правильное использование flex-shrink-0 для предотвращения collapse
|
||||||
|
|
||||||
|
### 5. **AuthPage оптимизация** (`src/pages/AuthPage.tsx`)
|
||||||
|
- Использование w-screen h-screen для полного заполнения экрана
|
||||||
|
- Уменьшены размеры логотипа на мобилах
|
||||||
|
- Оптимизированы кнопки с min-h-touch
|
||||||
|
|
||||||
|
### 6. **ProfilePage оптимизация** (`src/pages/ProfilePage.tsx`)
|
||||||
|
- Полноэкранное отображение (w-screen h-screen)
|
||||||
|
- Оптимизирована структура для лучшей прокрутки
|
||||||
|
- Уменьшены размеры иконок на мобилах
|
||||||
|
- Правильные размеры кнопок для touch устройств
|
||||||
|
|
||||||
|
### 7. **SettingsPage оптимизация** (`src/pages/SettingsPage.tsx`)
|
||||||
|
- Адаптивные размеры темных кнопок
|
||||||
|
- Оптимизированные опции тем для мобилы
|
||||||
|
- Улучшенная прокрутка содержимого
|
||||||
|
|
||||||
|
### 8. **App.tsx улучшения** (`src/App.tsx`)
|
||||||
|
- Добавлена обработка viewport resize при появлении клавиатуры
|
||||||
|
- Попытка предотвращения "bounce" скроллинга на iOS
|
||||||
|
- Установка CSS переменной `--vh` для правильного заполнения 100vh на мобилах
|
||||||
|
|
||||||
|
## 📱 Native-like особенности
|
||||||
|
|
||||||
|
### Визуальные улучшения
|
||||||
|
- 🎨 Правильные border-radius для iOS (`rounded-*` классы)
|
||||||
|
- 🎯 Touch-friendly размеры кнопок (минимум 44x44px)
|
||||||
|
- 📏 Оптимизированные шрифты для малых экранов
|
||||||
|
- 🪟 Поддержка safe area inset (notch, bottom bar)
|
||||||
|
|
||||||
|
### Поведение
|
||||||
|
- ⌨️ Правильная обработка появления/исчезновения клавиатуры
|
||||||
|
- 🖱️ Отключены неправильные ховер эффекты на мобилах
|
||||||
|
- ⚡ Оптимизирована производительность (GPU acceleration где необходимо)
|
||||||
|
- 🔄 Правильный скроллинг с инерцией на iOS
|
||||||
|
|
||||||
|
### Доступность
|
||||||
|
- ♿ Поддержка `prefers-reduced-motion`
|
||||||
|
- 👁️ Оптимизированные размеры текста
|
||||||
|
- 🔊 Правильные focus состояния
|
||||||
|
|
||||||
|
## 🚀 Рекомендации для дальнейшей оптимизации
|
||||||
|
|
||||||
|
### 1. Изображения
|
||||||
|
- Используйте WebP формат с fallback на PNG/JPG
|
||||||
|
- Реализуйте lazy loading для аватаров
|
||||||
|
- Оптимизируйте размеры изображений через srcset
|
||||||
|
|
||||||
|
### 2. Производительность
|
||||||
|
```javascript
|
||||||
|
// Рассмотрите использование React.memo для больших списков
|
||||||
|
const MessageBubble = React.memo(({ message, isMyMessage }) => {
|
||||||
|
// компонент
|
||||||
|
});
|
||||||
|
|
||||||
|
// Используйте useMemo для тяжелых вычислений
|
||||||
|
const sortedMessages = useMemo(() => {
|
||||||
|
return messages.sort((a, b) => new Date(a.created_at) - new Date(b.created_at));
|
||||||
|
}, [messages]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Network
|
||||||
|
- Реализуйте сжатие изображений на клиенте перед загрузкой
|
||||||
|
- Используйте Service Worker для кэширования
|
||||||
|
- Considere использование webp для аватаров
|
||||||
|
|
||||||
|
### 4. Storage
|
||||||
|
- Реализуйте IndexedDB для кэширования сообщений
|
||||||
|
- Используйте localStorage для сохранения черновиков
|
||||||
|
|
||||||
|
### 5. PWA
|
||||||
|
```json
|
||||||
|
// Добавьте manifest.json
|
||||||
|
{
|
||||||
|
"name": "Aether Messenger",
|
||||||
|
"short_name": "Aether",
|
||||||
|
"description": "Простой и элегантный мессенджер",
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"theme_color": "#6B705C",
|
||||||
|
"background_color": "#F5F5F1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. Тестирование
|
||||||
|
- ☑️ Тестируйте на реальных мобильных устройствах
|
||||||
|
- ☑️ Проверяйте на разных браузерах (Safari, Chrome, Samsung Internet)
|
||||||
|
- ☑️ Тестируйте с отключенным интернетом/медленной сетью
|
||||||
|
- ☑️ Используйте Chrome DevTools Mobile Emulation
|
||||||
|
|
||||||
|
## 🔍 Проверка результатов
|
||||||
|
|
||||||
|
### Используйте Chrome DevTools
|
||||||
|
1. Откройте DevTools (F12)
|
||||||
|
2. Переключитесь в Mobile режим (Ctrl+Shift+M)
|
||||||
|
3. Проверьте:
|
||||||
|
- ✅ Все элементы видны и правильно отцентрированы
|
||||||
|
- ✅ Нет горизонтального скроллинга
|
||||||
|
- ✅ Кнопки достаточно большие для нажатия
|
||||||
|
- ✅ Text читаемый без зума
|
||||||
|
|
||||||
|
### Lighthouse аудит
|
||||||
|
1. Откройте Lighthouse в Chrome DevTools
|
||||||
|
2. Запустите аудит для Mobile
|
||||||
|
3. Проверьте очки для Performance, Accessibility, Best Practices
|
||||||
|
|
||||||
|
## 📊 Тестовые устройства
|
||||||
|
|
||||||
|
Рекомендуется тестировать на:
|
||||||
|
- iPhone 11/12/13 (Safari)
|
||||||
|
- Samsung Galaxy S21/22 (Chrome)
|
||||||
|
- Pixel 6/7 (Chrome)
|
||||||
|
- iPad (Safari)
|
||||||
|
- Планшеты 10-12 дюймов
|
||||||
|
|
||||||
|
## 📝 Контрольный список перед релизом
|
||||||
|
|
||||||
|
- [ ] Тестировано на iPhone с iOS 14+
|
||||||
|
- [ ] Тестировано на Android 10+
|
||||||
|
- [ ] Все кнопки имеют минимум 44x44px
|
||||||
|
- [ ] Нет горизонтального скроллинга
|
||||||
|
- [ ] Модалы открываются снизу на мобилах
|
||||||
|
- [ ] Клавиатура не прячет поле ввода
|
||||||
|
- [ ] Изображения оптимизированы
|
||||||
|
- [ ] Touch события работают без задержек
|
||||||
|
- [ ] LighthouseScore > 85 для Mobile
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Версия:** 1.0
|
||||||
|
**Дата последнего обновления:** 24 февраля 2026
|
||||||
|
**Статус:** ✅ Завершено
|
||||||
+10
-2
@@ -3,13 +3,21 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1.0, user-scalable=no, shrink-to-fit=no" />
|
||||||
|
<meta name="theme-color" content="#6B705C" />
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
|
<meta name="apple-mobile-web-app-title" content="Aether" />
|
||||||
|
<link rel="apple-touch-icon" href="/favicon.png" />
|
||||||
|
<meta name="description" content="Aether — изящный мессенджер с простотой и красотой" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="format-detection" content="telephone=no" />
|
||||||
<title>Aether — Messenger</title>
|
<title>Aether — Messenger</title>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;1,400&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;1,400&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="antialiased">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+26
-2
@@ -17,7 +17,7 @@ function PrivateRoute({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center" style={{ backgroundColor: '#F5F5F1' }}>
|
<div className="w-full h-full flex items-center justify-center" style={{ backgroundColor: '#F5F5F1' }}>
|
||||||
<div className="w-16 h-16 border-4 border-gray-200 rounded-full animate-spin" style={{ borderTopColor: '#6B705C' }}></div>
|
<div className="w-16 h-16 border-4 border-gray-200 rounded-full animate-spin" style={{ borderTopColor: '#6B705C' }}></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -36,6 +36,30 @@ function App() {
|
|||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Prevent default iOS behaviors
|
||||||
|
document.addEventListener('touchmove', (e) => {
|
||||||
|
if ((e.target as any).closest('.overflow-y-auto') === null &&
|
||||||
|
(e.target as any).nodeName !== 'INPUT' &&
|
||||||
|
(e.target as any).nodeName !== 'TEXTAREA') {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}, { passive: false });
|
||||||
|
|
||||||
|
// Handle viewport resize on mobile (keyboard appears)
|
||||||
|
const handleResize = () => {
|
||||||
|
const vh = window.innerHeight * 0.01;
|
||||||
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleResize();
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkAuth = async () => {
|
const checkAuth = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -49,7 +73,7 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
checkAuth();
|
checkAuth();
|
||||||
}, []); // Пустой массив зависимостей - выполнится только один раз
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|||||||
+200
-1
@@ -41,9 +41,26 @@
|
|||||||
--error-soft: rgba(216, 155, 142, 0.15);
|
--error-soft: rgba(216, 155, 142, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
position: fixed;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100vh;
|
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -52,5 +69,187 @@ body {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
background-color: var(--bg-primary);
|
background-color: var(--bg-primary);
|
||||||
transition: background-color 0.3s ease, color 0.3s ease;
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
|
||||||
|
/* Prevent zoom on double tap and long press */
|
||||||
|
touch-action: manipulation;
|
||||||
|
|
||||||
|
/* Mobile optimization */
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
|
||||||
|
/* Notch support */
|
||||||
|
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text selection for inputs */
|
||||||
|
input, textarea, select {
|
||||||
|
-webkit-user-select: text;
|
||||||
|
user-select: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth scrolling */
|
||||||
|
@supports (scroll-behavior: smooth) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile touch optimizations */
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
/* Disable hover states on touch devices */
|
||||||
|
button:hover,
|
||||||
|
a:hover {
|
||||||
|
filter: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure minimum touch target size */
|
||||||
|
button,
|
||||||
|
input[type="button"],
|
||||||
|
input[type="submit"],
|
||||||
|
[role="button"] {
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent iOS default styling */
|
||||||
|
input,
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable blue tap highlight */
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
/* Better focus states for touch */
|
||||||
|
input:focus,
|
||||||
|
textarea:focus,
|
||||||
|
select:focus {
|
||||||
|
outline: 2px solid var(--accent-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent text zoom when rotating */
|
||||||
|
body {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth transitions */
|
||||||
|
* {
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal scrolling fix on iOS */
|
||||||
|
body.modal-open {
|
||||||
|
overflow: hidden;
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for iOS keyboard appearance */
|
||||||
|
@supports (padding: max(0px)) {
|
||||||
|
body {
|
||||||
|
padding-bottom: max(env(safe-area-inset-bottom), 0px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive font sizes */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
html {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) {
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better button interactivity on mobile */
|
||||||
|
button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input styling for mobile */
|
||||||
|
input[type="text"],
|
||||||
|
input[type="email"],
|
||||||
|
input[type="password"],
|
||||||
|
textarea {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable auto-zoom on iOS input focus */
|
||||||
|
input[type="text"],
|
||||||
|
input[type="email"],
|
||||||
|
input[type="password"],
|
||||||
|
textarea,
|
||||||
|
select {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth scrolling container */
|
||||||
|
.overflow-y-auto {
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better focus visible styling */
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 2px solid var(--accent-primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading spinner optimization */
|
||||||
|
@keyframes spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-spin {
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Performance: Use GPU acceleration */
|
||||||
|
.motion-reduce {
|
||||||
|
animation: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export default function AuthPage() {
|
|||||||
const [isLogin, setIsLogin] = useState(true);
|
const [isLogin, setIsLogin] = useState(true);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center p-3 md:p-4 relative" style={{ backgroundColor: '#F5F5F1' }}>
|
<div className="w-screen h-screen flex items-center justify-center p-3 md:p-4 relative overflow-hidden" style={{ backgroundColor: '#F5F5F1' }}>
|
||||||
{/* Subtle texture background */}
|
{/* Subtle texture background */}
|
||||||
<div className="absolute inset-0 opacity-30 pointer-events-none"
|
<div className="absolute inset-0 opacity-30 pointer-events-none"
|
||||||
style={{
|
style={{
|
||||||
@@ -23,19 +23,19 @@ export default function AuthPage() {
|
|||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
>
|
>
|
||||||
<div className="bg-card-white rounded-[24px] md:rounded-[32px] shadow-soft px-6 md:px-10 py-8 md:py-12">
|
<div className="bg-card-white rounded-2xl md:rounded-3xl shadow-soft px-5 md:px-10 py-8 md:py-12">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="text-center mb-6 md:mb-8">
|
<div className="text-center mb-5 md:mb-8">
|
||||||
<div className="auth-logo w-[80px] h-[80px] md:w-[100px] md:h-[100px] mx-auto mb-6 md:mb-8 flex items-center justify-center">
|
<div className="auth-logo w-16 h-16 md:w-24 md:h-24 mx-auto mb-4 md:mb-6 flex items-center justify-center">
|
||||||
<img src={miniLogo} alt="Aether Logo" className="w-full h-full object-contain" />
|
<img src={miniLogo} alt="Aether Logo" className="w-full h-full object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<div className="font-lora text-accent-olive text-base md:text-lg tracking-[2px] mb-4 md:mb-6">
|
<div className="font-lora text-accent-olive text-sm md:text-lg tracking-[2px] mb-3 md:mb-4">
|
||||||
AETHER
|
AETHER
|
||||||
</div>
|
</div>
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait">
|
||||||
<motion.h1
|
<motion.h1
|
||||||
key={isLogin ? 'login' : 'register'}
|
key={isLogin ? 'login' : 'register'}
|
||||||
className="font-lora font-semibold text-2xl md:text-[28px] text-text-main mb-2"
|
className="font-lora font-semibold text-xl md:text-2xl text-text-main mb-2"
|
||||||
initial={{ opacity: 0, y: -10 }}
|
initial={{ opacity: 0, y: -10 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: 10 }}
|
exit={{ opacity: 0, y: 10 }}
|
||||||
@@ -60,13 +60,13 @@ export default function AuthPage() {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* Switch */}
|
{/* Switch */}
|
||||||
<div className="mt-6 text-center text-sm font-inter" style={{ color: '#8B8B8B' }}>
|
<div className="mt-5 md:mt-6 text-center text-xs md:text-sm font-inter" style={{ color: '#8B8B8B' }}>
|
||||||
{isLogin ? (
|
{isLogin ? (
|
||||||
<>
|
<>
|
||||||
Ещё нет аккаунта?{' '}
|
Ещё нет аккаунта?{' '}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsLogin(false)}
|
onClick={() => setIsLogin(false)}
|
||||||
className="font-medium hover:underline transition"
|
className="font-medium hover:underline transition min-h-touch"
|
||||||
style={{ color: '#6B705C' }}
|
style={{ color: '#6B705C' }}
|
||||||
>
|
>
|
||||||
Зарегистрироваться
|
Зарегистрироваться
|
||||||
@@ -77,7 +77,7 @@ export default function AuthPage() {
|
|||||||
Уже есть аккаунт?{' '}
|
Уже есть аккаунт?{' '}
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsLogin(true)}
|
onClick={() => setIsLogin(true)}
|
||||||
className="font-medium hover:underline transition"
|
className="font-medium hover:underline transition min-h-touch"
|
||||||
style={{ color: '#6B705C' }}
|
style={{ color: '#6B705C' }}
|
||||||
>
|
>
|
||||||
Войти
|
Войти
|
||||||
|
|||||||
+116
-114
@@ -521,31 +521,31 @@ export default function ChatPage() {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen h-screen flex" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
<div className="w-screen h-screen flex flex-col md:flex-row overflow-hidden" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<div className={`w-full md:w-80 flex flex-col shadow-soft ${
|
<div className={`w-screen md:w-80 flex flex-col h-screen shadow-soft ${
|
||||||
selectedChat ? 'hidden md:flex' : 'flex'
|
selectedChat ? 'hidden md:flex' : 'flex'
|
||||||
}`} style={{ backgroundColor: 'var(--bg-card)' }}>
|
}`} style={{ backgroundColor: 'var(--bg-card)' }}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="p-4 md:p-6 border-b" style={{ borderColor: 'var(--border-color)' }}>
|
<div className="p-2.5 md:p-6 border-b flex-shrink-0" style={{ borderColor: 'var(--border-color)' }}>
|
||||||
<h1 className="text-2xl md:text-3xl font-lora font-semibold text-center tracking-wider" style={{ color: 'var(--accent-primary)' }}>
|
<h1 className="text-xl md:text-3xl font-lora font-semibold text-center tracking-wider" style={{ color: 'var(--accent-primary)' }}>
|
||||||
AETHER
|
AETHER
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Verification Banner */}
|
{/* Verification Banner */}
|
||||||
{user && !user.is_verified && (
|
{user && !user.is_verified && (
|
||||||
<div className="p-4">
|
<div className="px-3 py-2 md:p-4 flex-shrink-0">
|
||||||
<VerificationBanner userEmail={user.email} />
|
<VerificationBanner userEmail={user.email} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* User Profile Section */}
|
{/* User Profile Section */}
|
||||||
<div className="p-4 md:p-6 border-b" style={{ borderColor: 'var(--border-color)' }}>
|
<div className="px-3 py-3 md:p-6 border-b flex-shrink-0" style={{ borderColor: 'var(--border-color)' }}>
|
||||||
<div className="flex items-center gap-3 md:gap-4">
|
<div className="flex items-center gap-2 md:gap-4">
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div
|
<div
|
||||||
className="w-12 h-12 md:w-14 md:h-14 flex items-center justify-center flex-shrink-0 cursor-pointer hover:opacity-90 transition"
|
className="w-10 h-10 md:w-14 md:h-14 flex items-center justify-center flex-shrink-0 cursor-pointer hover:opacity-90 transition"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: user?.avatar_url ? `url(${user.avatar_url})` : undefined,
|
backgroundImage: user?.avatar_url ? `url(${user.avatar_url})` : undefined,
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
@@ -562,10 +562,10 @@ export default function ChatPage() {
|
|||||||
|
|
||||||
{/* User Info */}
|
{/* User Info */}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-inter font-semibold truncate" style={{ color: 'var(--text-primary)' }}>
|
<h3 className="font-inter font-semibold text-sm md:text-base truncate" style={{ color: 'var(--text-primary)' }}>
|
||||||
{user?.display_name || user?.username}
|
{user?.display_name || user?.username}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm font-inter truncate" style={{ color: 'var(--text-secondary)' }}>
|
<p className="text-xs md:text-sm font-inter truncate" style={{ color: 'var(--text-secondary)' }}>
|
||||||
@{user?.username}
|
@{user?.username}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -573,61 +573,61 @@ export default function ChatPage() {
|
|||||||
{/* Settings Icon */}
|
{/* Settings Icon */}
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/settings')}
|
onClick={() => navigate('/settings')}
|
||||||
className="p-2 hover:bg-gray-100 rounded-full transition"
|
className="p-1.5 md:p-2 hover:bg-gray-100 rounded-full transition min-h-touch min-w-touch"
|
||||||
title="Настройки"
|
title="Настройки"
|
||||||
>
|
>
|
||||||
<Settings size={20} style={{ color: 'var(--text-secondary)' }} />
|
<Settings size={18} style={{ color: 'var(--text-secondary)' }} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action Button */}
|
{/* Action Button */}
|
||||||
<div className="mt-4">
|
<div className="mt-2 md:mt-4">
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => navigate('/profile')}
|
onClick={() => navigate('/profile')}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
className="w-full flex items-center justify-center gap-2 py-2 px-3 rounded-xl font-inter text-sm font-medium transition hover:opacity-80"
|
className="w-full flex items-center justify-center gap-2 py-2 px-3 rounded-xl md:rounded-2xl font-inter text-xs md:text-sm font-medium transition hover:opacity-80 min-h-touch"
|
||||||
style={{ backgroundColor: 'var(--bg-input)', color: 'var(--accent-primary)' }}
|
style={{ backgroundColor: 'var(--bg-input)', color: 'var(--accent-primary)' }}
|
||||||
>
|
>
|
||||||
<User size={16} />
|
<User size={14} className="md:w-4 md:h-4" />
|
||||||
Профиль
|
Профиль
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* New Chat Button */}
|
{/* New Chat Button */}
|
||||||
<div className="p-4">
|
<div className="px-3 py-2 md:p-4 flex-shrink-0">
|
||||||
<motion.button
|
<motion.button
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={() => setShowNewChatModal(true)}
|
onClick={() => setShowNewChatModal(true)}
|
||||||
className="w-full flex items-center justify-center gap-3 py-3 px-4 rounded-2xl font-inter font-semibold shadow-sm hover:shadow-md transition"
|
className="w-full flex items-center justify-center gap-2 md:gap-3 py-2.5 md:py-3 px-3 md:px-4 rounded-xl md:rounded-2xl font-inter font-semibold text-sm md:text-base shadow-sm hover:shadow-md transition min-h-touch"
|
||||||
style={{ backgroundColor: 'var(--accent-primary)', color: 'white' }}
|
style={{ backgroundColor: 'var(--accent-primary)', color: 'white' }}
|
||||||
>
|
>
|
||||||
<MessageSquarePlus size={20} />
|
<MessageSquarePlus size={16} className="md:w-5 md:h-5" />
|
||||||
Новый чат
|
Новый чат
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chats List */}
|
{/* Chats List */}
|
||||||
<div className="flex-1 overflow-y-auto px-4">
|
<div className="flex-1 overflow-y-auto px-2 md:px-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-1 md:space-y-2">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-center py-12 px-4">
|
<div className="text-center py-12 px-4">
|
||||||
<div className="animate-spin w-8 h-8 mx-auto mb-4 border-2 border-t-transparent rounded-full"
|
<div className="animate-spin w-8 h-8 mx-auto mb-4 border-2 border-t-transparent rounded-full"
|
||||||
style={{ borderColor: 'var(--accent-primary)', borderTopColor: 'transparent' }}></div>
|
style={{ borderColor: 'var(--accent-primary)', borderTopColor: 'transparent' }}></div>
|
||||||
<p className="font-inter text-sm" style={{ color: 'var(--text-secondary)' }}>
|
<p className="font-inter text-xs md:text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
Загрузка чатов...
|
Загрузка чатов...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="text-center py-12 px-4">
|
<div className="text-center py-12 px-4">
|
||||||
<p className="font-inter text-sm text-red-500">{error}</p>
|
<p className="font-inter text-xs md:text-sm text-red-500">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
) : chats.length === 0 ? (
|
) : chats.length === 0 ? (
|
||||||
<div className="text-center py-12 px-4">
|
<div className="text-center py-12 px-4">
|
||||||
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-gradient-to-br from-accent-terracotta/20 to-accent-olive/20 flex items-center justify-center">
|
<div className="w-16 h-16 mx-auto mb-4 rounded-full bg-gradient-to-br from-accent-terracotta/20 to-accent-olive/20 flex items-center justify-center">
|
||||||
<MessageSquarePlus size={32} style={{ color: 'var(--accent-primary)', opacity: 0.5 }} />
|
<MessageSquarePlus size={32} style={{ color: 'var(--accent-primary)', opacity: 0.5 }} />
|
||||||
</div>
|
</div>
|
||||||
<p className="font-inter text-sm" style={{ color: 'var(--text-secondary)' }}>
|
<p className="font-inter text-xs md:text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
Пока нет чатов
|
Пока нет чатов
|
||||||
</p>
|
</p>
|
||||||
<p className="font-inter text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
|
<p className="font-inter text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
|
||||||
@@ -641,17 +641,17 @@ export default function ChatPage() {
|
|||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={() => handleChatClick(chat)}
|
onClick={() => handleChatClick(chat)}
|
||||||
className="p-4 rounded-2xl cursor-pointer transition-all relative overflow-hidden"
|
className="p-2.5 md:p-4 rounded-lg md:rounded-2xl cursor-pointer transition-all relative overflow-hidden min-h-touch"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: selectedChat?.chat_id === chat.chat_id ? 'var(--accent-primary)' : 'var(--bg-input)',
|
backgroundColor: selectedChat?.chat_id === chat.chat_id ? 'var(--accent-primary)' : 'var(--bg-input)',
|
||||||
boxShadow: selectedChat?.chat_id === chat.chat_id ? '0 4px 12px rgba(0,0,0,0.1)' : 'none',
|
boxShadow: selectedChat?.chat_id === chat.chat_id ? '0 4px 12px rgba(0,0,0,0.1)' : 'none',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-2.5 md:gap-4">
|
||||||
{/* Chat Avatar with Online Indicator */}
|
{/* Chat Avatar with Online Indicator */}
|
||||||
<div className="relative flex-shrink-0">
|
<div className="relative flex-shrink-0">
|
||||||
<div
|
<div
|
||||||
className="w-12 h-12 flex items-center justify-center"
|
className="w-10 h-10 md:w-12 md:h-12 flex items-center justify-center"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: chat.avatar_url ? `url(${chat.avatar_url})` : undefined,
|
backgroundImage: chat.avatar_url ? `url(${chat.avatar_url})` : undefined,
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
@@ -665,7 +665,7 @@ export default function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
{/* Online indicator */}
|
{/* Online indicator */}
|
||||||
<div
|
<div
|
||||||
className="absolute bottom-0 right-0 w-4 h-4 rounded-full border-2"
|
className="absolute bottom-0 right-0 w-3 h-3 md:w-4 md:h-4 rounded-full border-2"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: '#10b981',
|
backgroundColor: '#10b981',
|
||||||
borderColor: 'var(--bg-card)'
|
borderColor: 'var(--bg-card)'
|
||||||
@@ -677,7 +677,7 @@ export default function ChatPage() {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-start justify-between gap-2 mb-1">
|
<div className="flex items-start justify-between gap-2 mb-1">
|
||||||
<h4
|
<h4
|
||||||
className="font-inter font-semibold text-base truncate"
|
className="font-inter font-semibold text-sm md:text-base truncate"
|
||||||
style={{ color: selectedChat?.chat_id === chat.chat_id ? 'white' : 'var(--text-primary)' }}
|
style={{ color: selectedChat?.chat_id === chat.chat_id ? 'white' : 'var(--text-primary)' }}
|
||||||
>
|
>
|
||||||
{chat.display_name}
|
{chat.display_name}
|
||||||
@@ -696,14 +696,14 @@ export default function ChatPage() {
|
|||||||
|
|
||||||
{chat.last_message ? (
|
{chat.last_message ? (
|
||||||
<p
|
<p
|
||||||
className="font-inter text-sm truncate"
|
className="font-inter text-xs md:text-sm truncate"
|
||||||
style={{ color: selectedChat?.chat_id === chat.chat_id ? 'rgba(255,255,255,0.85)' : 'var(--text-secondary)' }}
|
style={{ color: selectedChat?.chat_id === chat.chat_id ? 'rgba(255,255,255,0.85)' : 'var(--text-secondary)' }}
|
||||||
>
|
>
|
||||||
{chat.last_message}
|
{chat.last_message}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p
|
<p
|
||||||
className="font-inter text-sm italic truncate"
|
className="font-inter text-xs md:text-sm italic truncate"
|
||||||
style={{ color: selectedChat?.chat_id === chat.chat_id ? 'rgba(255,255,255,0.6)' : 'var(--text-secondary)' }}
|
style={{ color: selectedChat?.chat_id === chat.chat_id ? 'rgba(255,255,255,0.6)' : 'var(--text-secondary)' }}
|
||||||
>
|
>
|
||||||
Нет сообщений
|
Нет сообщений
|
||||||
@@ -718,7 +718,7 @@ export default function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer Info */}
|
{/* Footer Info */}
|
||||||
<div className="p-4 border-t text-center" style={{ borderColor: 'var(--border-color)' }}>
|
<div className="p-2 md:p-4 border-t text-center flex-shrink-0" style={{ borderColor: 'var(--border-color)' }}>
|
||||||
<p className="text-xs font-inter" style={{ color: 'var(--text-secondary)' }}>
|
<p className="text-xs font-inter" style={{ color: 'var(--text-secondary)' }}>
|
||||||
Aether Chat v1.0
|
Aether Chat v1.0
|
||||||
</p>
|
</p>
|
||||||
@@ -726,13 +726,13 @@ export default function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Chat Area */}
|
{/* Main Chat Area */}
|
||||||
<div className={`flex-1 flex flex-col relative ${
|
<div className={`flex-1 flex flex-col h-screen relative ${
|
||||||
selectedChat ? 'flex' : 'hidden md:flex'
|
selectedChat ? 'flex' : 'hidden md:flex'
|
||||||
}`}>
|
}`}>
|
||||||
{selectedChat ? (
|
{selectedChat ? (
|
||||||
<>
|
<>
|
||||||
{/* Chat Header */}
|
{/* Chat Header */}
|
||||||
<div className="p-3 md:p-4 border-b flex items-center gap-2 md:gap-4" style={{
|
<div className="px-2 md:px-4 py-2 md:py-4 border-b flex items-center gap-2 md:gap-4 flex-shrink-0" style={{
|
||||||
backgroundColor: 'var(--bg-card)',
|
backgroundColor: 'var(--bg-card)',
|
||||||
borderColor: 'var(--border-color)'
|
borderColor: 'var(--border-color)'
|
||||||
}}>
|
}}>
|
||||||
@@ -740,21 +740,21 @@ export default function ChatPage() {
|
|||||||
whileHover={{ scale: 1.1 }}
|
whileHover={{ scale: 1.1 }}
|
||||||
whileTap={{ scale: 0.9 }}
|
whileTap={{ scale: 0.9 }}
|
||||||
onClick={handleBackToChats}
|
onClick={handleBackToChats}
|
||||||
className="p-2 rounded-full transition"
|
className="p-2 rounded-full transition min-h-touch min-w-touch"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--bg-input)',
|
backgroundColor: 'var(--bg-input)',
|
||||||
color: 'var(--text-primary)'
|
color: 'var(--text-primary)'
|
||||||
}}
|
}}
|
||||||
title="Назад к чатам"
|
title="Назад к чатам"
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={18} className="md:w-5 md:h-5" />
|
||||||
</motion.button>
|
</motion.button>
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ scale: 1.05 }}
|
whileHover={{ scale: 1.05 }}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
onClick={() => handleViewUserProfile(selectedChat.user_id)}
|
onClick={() => handleViewUserProfile(selectedChat.user_id)}
|
||||||
className="w-12 h-12 flex-shrink-0 flex items-center justify-center cursor-pointer"
|
className="w-10 h-10 md:w-12 md:h-12 flex-shrink-0 flex items-center justify-center cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: selectedChat.avatar_url ? `url(${selectedChat.avatar_url})` : undefined,
|
backgroundImage: selectedChat.avatar_url ? `url(${selectedChat.avatar_url})` : undefined,
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
@@ -771,7 +771,7 @@ export default function ChatPage() {
|
|||||||
className="flex-1 min-w-0 cursor-pointer"
|
className="flex-1 min-w-0 cursor-pointer"
|
||||||
onClick={() => handleViewUserProfile(selectedChat.user_id)}
|
onClick={() => handleViewUserProfile(selectedChat.user_id)}
|
||||||
>
|
>
|
||||||
<h3 className="font-inter font-semibold truncate hover:underline" style={{ color: 'var(--text-primary)' }}>
|
<h3 className="font-inter font-semibold text-sm md:text-base truncate hover:underline" style={{ color: 'var(--text-primary)' }}>
|
||||||
{selectedChat.display_name}
|
{selectedChat.display_name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs font-inter" style={{ color: 'var(--text-secondary)' }}>
|
<p className="text-xs font-inter" style={{ color: 'var(--text-secondary)' }}>
|
||||||
@@ -784,7 +784,7 @@ export default function ChatPage() {
|
|||||||
<div
|
<div
|
||||||
ref={messagesContainerRef}
|
ref={messagesContainerRef}
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
className="flex-1 overflow-y-auto p-6 relative"
|
className="flex-1 overflow-y-auto px-3 md:px-6 py-4 md:py-6 relative"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--bg-primary)',
|
backgroundColor: 'var(--bg-primary)',
|
||||||
backgroundImage: `
|
backgroundImage: `
|
||||||
@@ -848,8 +848,8 @@ export default function ChatPage() {
|
|||||||
<div key={message.id}>
|
<div key={message.id}>
|
||||||
{/* Date divider */}
|
{/* Date divider */}
|
||||||
{showDateDivider && (
|
{showDateDivider && (
|
||||||
<div className="flex items-center justify-center my-6">
|
<div className="flex items-center justify-center my-4 md:my-6">
|
||||||
<div className="px-4 py-2 rounded-full font-inter text-xs font-medium shadow-sm"
|
<div className="px-3 md:px-4 py-1.5 md:py-2 rounded-full font-inter text-xs font-medium shadow-sm"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--bg-card)',
|
backgroundColor: 'var(--bg-card)',
|
||||||
color: 'var(--text-secondary)',
|
color: 'var(--text-secondary)',
|
||||||
@@ -864,7 +864,7 @@ export default function ChatPage() {
|
|||||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||||
transition={{ duration: 0.4, ease: "easeOut" }}
|
transition={{ duration: 0.4, ease: "easeOut" }}
|
||||||
className={`flex gap-3 mb-3 ${isMyMessage ? 'justify-end' : 'justify-start'} ${!showAvatar && !isMyMessage ? 'ml-11' : ''}`}
|
className={`flex gap-2 md:gap-3 mb-2 md:mb-3 ${isMyMessage ? 'justify-end' : 'justify-start'} ${!showAvatar && !isMyMessage ? 'ml-8 md:ml-11' : ''}`}
|
||||||
>
|
>
|
||||||
{/* Avatar for incoming messages */}
|
{/* Avatar for incoming messages */}
|
||||||
{!isMyMessage && showAvatar && (
|
{!isMyMessage && showAvatar && (
|
||||||
@@ -872,7 +872,7 @@ export default function ChatPage() {
|
|||||||
initial={{ scale: 0 }}
|
initial={{ scale: 0 }}
|
||||||
animate={{ scale: 1 }}
|
animate={{ scale: 1 }}
|
||||||
transition={{ delay: 0.1 }}
|
transition={{ delay: 0.1 }}
|
||||||
className="w-9 h-9 flex-shrink-0 flex items-center justify-center self-end shadow-md"
|
className="w-7 h-7 md:w-9 md:h-9 flex-shrink-0 flex items-center justify-center self-end shadow-md"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: selectedChat.avatar_url ? `url(${selectedChat.avatar_url})` : undefined,
|
backgroundImage: selectedChat.avatar_url ? `url(${selectedChat.avatar_url})` : undefined,
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
@@ -888,20 +888,20 @@ export default function ChatPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Message bubble */}
|
{/* Message bubble */}
|
||||||
<div className="flex flex-col max-w-lg">
|
<div className="flex flex-col max-w-xs md:max-w-lg">
|
||||||
{/* Sender name for incoming messages */}
|
{/* Sender name for incoming messages */}
|
||||||
{!isMyMessage && showAvatar && (
|
{!isMyMessage && showAvatar && (
|
||||||
<span className="text-xs font-inter font-medium mb-1 px-2" style={{ color: 'var(--accent-primary)' }}>
|
<span className="text-xs font-inter font-medium mb-0.5 md:mb-1 px-2" style={{ color: 'var(--accent-primary)' }}>
|
||||||
{selectedChat.display_name}
|
{selectedChat.display_name}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
className={`px-5 py-3 font-inter text-[15px] leading-relaxed relative group ${
|
className={`px-3 md:px-5 py-2 md:py-3 font-inter text-sm md:text-[15px] leading-relaxed relative group ${
|
||||||
isMyMessage
|
isMyMessage
|
||||||
? 'rounded-[20px] rounded-br-md'
|
? 'rounded-2xl md:rounded-[20px] rounded-br-sm'
|
||||||
: 'rounded-[20px] rounded-bl-md'
|
: 'rounded-2xl md:rounded-[20px] rounded-bl-sm'
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: isMyMessage ? 'var(--accent-primary)' : 'var(--bg-card)',
|
backgroundColor: isMyMessage ? 'var(--accent-primary)' : 'var(--bg-card)',
|
||||||
@@ -912,38 +912,38 @@ export default function ChatPage() {
|
|||||||
border: isMyMessage ? 'none' : '1px solid var(--border-color)'
|
border: isMyMessage ? 'none' : '1px solid var(--border-color)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Edit and Delete buttons - показываем только для своих сообщений */}
|
{/* Edit and Delete buttons - только для своих сообщений на десктопе */}
|
||||||
{isMyMessage && editingMessageId !== message.id && (
|
{isMyMessage && editingMessageId !== message.id && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleStartEdit(message)}
|
onClick={() => handleStartEdit(message)}
|
||||||
className="absolute -top-2 -right-10 p-1.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity shadow-md"
|
className="absolute -top-2 -right-10 p-1.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity shadow-md hidden md:block"
|
||||||
style={{ backgroundColor: 'var(--bg-card)', color: 'var(--accent-primary)' }}
|
style={{ backgroundColor: 'var(--bg-card)', color: 'var(--accent-primary)' }}
|
||||||
title="Редактировать"
|
title="Редактировать"
|
||||||
>
|
>
|
||||||
<Pencil size={14} />
|
<Pencil size={12} className="md:w-3.5 md:h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteMessage(message.id)}
|
onClick={() => handleDeleteMessage(message.id)}
|
||||||
className="absolute -top-2 -right-2 p-1.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity shadow-md"
|
className="absolute -top-2 -right-2 p-1.5 rounded-full opacity-0 group-hover:opacity-100 transition-opacity shadow-md hidden md:block"
|
||||||
style={{ backgroundColor: 'var(--bg-card)', color: '#DC2626' }}
|
style={{ backgroundColor: 'var(--bg-card)', color: '#DC2626' }}
|
||||||
title="Удалить"
|
title="Удалить"
|
||||||
>
|
>
|
||||||
<Trash2 size={14} />
|
<Trash2 size={12} className="md:w-3.5 md:h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Message content or edit input */}
|
{/* Message content or edit input */}
|
||||||
{editingMessageId === message.id ? (
|
{editingMessageId === message.id ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1 md:gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={editingMessageText}
|
value={editingMessageText}
|
||||||
onChange={(e) => setEditingMessageText(e.target.value)}
|
onChange={(e) => setEditingMessageText(e.target.value)}
|
||||||
onKeyDown={(e) => handleEditKeyPress(e, message.id)}
|
onKeyDown={(e) => handleEditKeyPress(e, message.id)}
|
||||||
autoFocus
|
autoFocus
|
||||||
className="flex-1 bg-transparent border-b-2 outline-none"
|
className="flex-1 bg-transparent border-b-2 outline-none text-sm md:text-base"
|
||||||
style={{
|
style={{
|
||||||
borderColor: isMyMessage ? 'rgba(255,255,255,0.5)' : 'var(--accent-primary)',
|
borderColor: isMyMessage ? 'rgba(255,255,255,0.5)' : 'var(--accent-primary)',
|
||||||
color: isMyMessage ? 'white' : 'var(--text-primary)'
|
color: isMyMessage ? 'white' : 'var(--text-primary)'
|
||||||
@@ -951,17 +951,17 @@ export default function ChatPage() {
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSaveEdit(message.id)}
|
onClick={() => handleSaveEdit(message.id)}
|
||||||
className="p-1 hover:opacity-70 transition"
|
className="p-1 hover:opacity-70 transition min-w-touch min-h-touch flex items-center justify-center"
|
||||||
title="Сохранить (Enter)"
|
title="Сохранить (Enter)"
|
||||||
>
|
>
|
||||||
<Check size={16} />
|
<Check size={14} className="md:w-4 md:h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleCancelEdit}
|
onClick={handleCancelEdit}
|
||||||
className="p-1 hover:opacity-70 transition"
|
className="p-1 hover:opacity-70 transition min-w-touch min-h-touch flex items-center justify-center"
|
||||||
title="Отмена (Esc)"
|
title="Отмена (Esc)"
|
||||||
>
|
>
|
||||||
<X size={16} />
|
<X size={14} className="md:w-4 md:h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -969,11 +969,11 @@ export default function ChatPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Message metadata */}
|
{/* Message metadata */}
|
||||||
<div className={`flex items-center gap-2 mt-1 ${isMyMessage ? 'justify-end' : 'justify-start'}`}>
|
<div className={`flex items-center gap-1 md:gap-2 mt-0.5 md:mt-1 ${isMyMessage ? 'justify-end' : 'justify-start'}`}>
|
||||||
{/* Edited indicator */}
|
{/* Edited indicator */}
|
||||||
{message.is_edited && (
|
{message.is_edited && (
|
||||||
<span
|
<span
|
||||||
className="text-[11px] font-inter font-medium italic"
|
className="text-[10px] md:text-[11px] font-inter font-medium italic"
|
||||||
style={{
|
style={{
|
||||||
color: isMyMessage ? 'rgba(255,255,255,0.6)' : 'var(--text-secondary)',
|
color: isMyMessage ? 'rgba(255,255,255,0.6)' : 'var(--text-secondary)',
|
||||||
}}
|
}}
|
||||||
@@ -983,7 +983,7 @@ export default function ChatPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className="text-[11px] font-inter font-medium"
|
className="text-[10px] md:text-[11px] font-inter font-medium"
|
||||||
style={{
|
style={{
|
||||||
color: isMyMessage ? 'rgba(255,255,255,0.75)' : 'var(--text-secondary)',
|
color: isMyMessage ? 'rgba(255,255,255,0.75)' : 'var(--text-secondary)',
|
||||||
}}
|
}}
|
||||||
@@ -997,8 +997,8 @@ export default function ChatPage() {
|
|||||||
{/* Read status for my messages */}
|
{/* Read status for my messages */}
|
||||||
{isMyMessage && (
|
{isMyMessage && (
|
||||||
<svg
|
<svg
|
||||||
width="16"
|
width="14"
|
||||||
height="16"
|
height="14"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
fill="none"
|
fill="none"
|
||||||
style={{ opacity: 0.75 }}
|
style={{ opacity: 0.75 }}
|
||||||
@@ -1022,7 +1022,7 @@ export default function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Reactions placeholder - можно добавить позже */}
|
{/* Reactions placeholder */}
|
||||||
{isLastInGroup && false && (
|
{isLastInGroup && false && (
|
||||||
<div className="flex gap-1 mt-1 px-2">
|
<div className="flex gap-1 mt-1 px-2">
|
||||||
<span className="text-xs">❤️</span>
|
<span className="text-xs">❤️</span>
|
||||||
@@ -1076,7 +1076,7 @@ export default function ChatPage() {
|
|||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
{/* Message Input */}
|
{/* Message Input */}
|
||||||
<div className="p-3 md:p-4 border-t" style={{
|
<div className="px-2 md:px-4 py-2 md:py-4 border-t flex-shrink-0" style={{
|
||||||
backgroundColor: 'var(--bg-card)',
|
backgroundColor: 'var(--bg-card)',
|
||||||
borderColor: 'var(--border-color)'
|
borderColor: 'var(--border-color)'
|
||||||
}}>
|
}}>
|
||||||
@@ -1087,13 +1087,14 @@ export default function ChatPage() {
|
|||||||
value={messageText}
|
value={messageText}
|
||||||
onChange={(e) => setMessageText(e.target.value)}
|
onChange={(e) => setMessageText(e.target.value)}
|
||||||
onKeyPress={handleKeyPress}
|
onKeyPress={handleKeyPress}
|
||||||
placeholder="Написать сообщение..."
|
placeholder="Сообщение..."
|
||||||
disabled={sendingMessage}
|
disabled={sendingMessage}
|
||||||
className="flex-1 px-3 md:px-4 py-2.5 md:py-3 rounded-2xl font-inter text-sm outline-none transition"
|
className="flex-1 px-3 md:px-4 py-2 md:py-3 rounded-lg md:rounded-2xl font-inter text-sm md:text-base outline-none transition min-h-touch"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--bg-input)',
|
backgroundColor: 'var(--bg-input)',
|
||||||
color: 'var(--text-primary)',
|
color: 'var(--text-primary)',
|
||||||
borderBottom: '2px solid transparent',
|
borderBottom: '2px solid transparent',
|
||||||
|
fontSize: '16px', // Prevent iOS zoom on input focus
|
||||||
}}
|
}}
|
||||||
onFocus={(e) => e.target.style.borderBottomColor = 'var(--accent-primary)'}
|
onFocus={(e) => e.target.style.borderBottomColor = 'var(--accent-primary)'}
|
||||||
onBlur={(e) => e.target.style.borderBottomColor = 'transparent'}
|
onBlur={(e) => e.target.style.borderBottomColor = 'transparent'}
|
||||||
@@ -1102,13 +1103,13 @@ export default function ChatPage() {
|
|||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
onClick={handleSendMessage}
|
onClick={handleSendMessage}
|
||||||
disabled={!messageText.trim() || sendingMessage}
|
disabled={!messageText.trim() || sendingMessage}
|
||||||
className="px-4 md:px-6 py-2.5 md:py-3 rounded-2xl font-inter font-semibold flex items-center gap-2 transition hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed"
|
className="px-3 md:px-6 py-2 md:py-3 rounded-lg md:rounded-2xl font-inter font-semibold flex items-center justify-center gap-1 md:gap-2 transition hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed min-h-touch min-w-touch"
|
||||||
style={{ backgroundColor: 'var(--accent-primary)', color: 'white' }}
|
style={{ backgroundColor: 'var(--accent-primary)', color: 'white' }}
|
||||||
>
|
>
|
||||||
{sendingMessage ? (
|
{sendingMessage ? (
|
||||||
<div className="animate-spin w-5 h-5 border-2 border-white border-t-transparent rounded-full"></div>
|
<div className="animate-spin w-4 h-4 md:w-5 md:h-5 border-2 border-white border-t-transparent rounded-full"></div>
|
||||||
) : (
|
) : (
|
||||||
<Send size={18} />
|
<Send size={16} className="md:w-4.5 md:h-4.5" />
|
||||||
)}
|
)}
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
@@ -1145,38 +1146,38 @@ export default function ChatPage() {
|
|||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
|
className="fixed inset-0 bg-black/50 flex items-end md:items-center justify-center z-50 p-4"
|
||||||
onClick={() => setViewingUser(null)}
|
onClick={() => setViewingUser(null)}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0.9, opacity: 0 }}
|
initial={{ scale: 0.9, opacity: 0, y: 100 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||||
exit={{ scale: 0.9, opacity: 0 }}
|
exit={{ scale: 0.9, opacity: 0, y: 100 }}
|
||||||
transition={{ type: "spring", duration: 0.3 }}
|
transition={{ type: "spring", duration: 0.3 }}
|
||||||
className="rounded-3xl shadow-2xl max-w-md w-full overflow-hidden"
|
className="rounded-t-3xl md:rounded-3xl shadow-2xl w-full md:max-w-md overflow-hidden"
|
||||||
style={{ backgroundColor: 'var(--bg-card)' }}
|
style={{ backgroundColor: 'var(--bg-card)' }}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className="relative p-6 border-b" style={{ borderColor: 'var(--border-color)' }}>
|
<div className="relative p-4 md:p-6 border-b" style={{ borderColor: 'var(--border-color)' }}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setViewingUser(null)}
|
onClick={() => setViewingUser(null)}
|
||||||
className="absolute top-4 right-4 p-2 rounded-full hover:bg-gray-100 transition"
|
className="absolute top-3 md:top-4 right-3 md:right-4 p-2 rounded-full hover:bg-gray-100 transition min-h-touch min-w-touch"
|
||||||
style={{ color: 'var(--text-secondary)' }}
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
>
|
>
|
||||||
<X size={20} />
|
<X size={18} className="md:w-5 md:h-5" />
|
||||||
</button>
|
</button>
|
||||||
<h2 className="text-xl font-lora font-semibold" style={{ color: 'var(--text-primary)' }}>
|
<h2 className="text-lg md:text-xl font-lora font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||||||
Профиль пользователя
|
Профиль пользователя
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal Content */}
|
{/* Modal Content */}
|
||||||
<div className="p-6">
|
<div className="p-4 md:p-6">
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div className="flex justify-center mb-6">
|
<div className="flex justify-center mb-4 md:mb-6">
|
||||||
<div
|
<div
|
||||||
className="w-32 h-32 rounded-full flex items-center justify-center shadow-lg"
|
className="w-24 h-24 md:w-32 md:h-32 rounded-full flex items-center justify-center shadow-lg"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: viewingUser.avatar_url ? `url(${viewingUser.avatar_url})` : undefined,
|
backgroundImage: viewingUser.avatar_url ? `url(${viewingUser.avatar_url})` : undefined,
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
@@ -1185,32 +1186,32 @@ export default function ChatPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!viewingUser.avatar_url && (
|
{!viewingUser.avatar_url && (
|
||||||
<User size={48} style={{ color: 'var(--text-secondary)' }} />
|
<User size={36} className="md:w-12 md:h-12" style={{ color: 'var(--text-secondary)' }} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User Info */}
|
{/* User Info */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-3 md:space-y-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h3 className="text-2xl font-lora font-semibold mb-1" style={{ color: 'var(--text-primary)' }}>
|
<h3 className="text-lg md:text-2xl font-lora font-semibold mb-0.5 md:mb-1" style={{ color: 'var(--text-primary)' }}>
|
||||||
{viewingUser.display_name}
|
{viewingUser.display_name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm font-inter" style={{ color: 'var(--text-secondary)' }}>
|
<p className="text-xs md:text-sm font-inter" style={{ color: 'var(--text-secondary)' }}>
|
||||||
@{viewingUser.username}
|
@{viewingUser.username}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewingUser.description && (
|
{viewingUser.description && (
|
||||||
<div className="p-4 rounded-2xl" style={{ backgroundColor: 'var(--bg-input)' }}>
|
<div className="p-3 md:p-4 rounded-lg md:rounded-2xl" style={{ backgroundColor: 'var(--bg-input)' }}>
|
||||||
<p className="text-sm font-inter" style={{ color: 'var(--text-primary)' }}>
|
<p className="text-sm md:text-base font-inter" style={{ color: 'var(--text-primary)' }}>
|
||||||
{viewingUser.description}
|
{viewingUser.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{viewingUser.birth_day && (
|
{viewingUser.birth_day && (
|
||||||
<div className="flex items-center gap-2 text-sm font-inter" style={{ color: 'var(--text-secondary)' }}>
|
<div className="flex items-center gap-2 text-xs md:text-sm font-inter" style={{ color: 'var(--text-secondary)' }}>
|
||||||
<span>🎂</span>
|
<span>🎂</span>
|
||||||
<span>
|
<span>
|
||||||
{new Date(viewingUser.birth_day).toLocaleDateString('ru-RU', {
|
{new Date(viewingUser.birth_day).toLocaleDateString('ru-RU', {
|
||||||
@@ -1222,18 +1223,18 @@ export default function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2 text-sm font-inter" style={{ color: 'var(--text-secondary)' }}>
|
<div className="flex items-center gap-2 text-xs md:text-sm font-inter" style={{ color: 'var(--text-secondary)' }}>
|
||||||
<span>✉️</span>
|
<span>✉️</span>
|
||||||
<span>{viewingUser.email}</span>
|
<span className="truncate">{viewingUser.email}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="mt-6 pt-6 border-t" style={{ borderColor: 'var(--border-color)' }}>
|
<div className="mt-4 md:mt-6 pt-4 md:pt-6 border-t" style={{ borderColor: 'var(--border-color)' }}>
|
||||||
<motion.button
|
<motion.button
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
onClick={() => setViewingUser(null)}
|
onClick={() => setViewingUser(null)}
|
||||||
className="w-full py-3 px-4 rounded-2xl font-inter font-semibold transition hover:opacity-90"
|
className="w-full py-2.5 md:py-3 px-4 rounded-lg md:rounded-2xl font-inter font-semibold transition hover:opacity-90 min-h-touch"
|
||||||
style={{ backgroundColor: 'var(--accent-primary)', color: 'white' }}
|
style={{ backgroundColor: 'var(--accent-primary)', color: 'white' }}
|
||||||
>
|
>
|
||||||
Закрыть
|
Закрыть
|
||||||
@@ -1252,7 +1253,7 @@ export default function ChatPage() {
|
|||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
|
className="fixed inset-0 bg-black/50 flex items-end md:items-center justify-center z-50 p-4"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowNewChatModal(false);
|
setShowNewChatModal(false);
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
@@ -1260,17 +1261,17 @@ export default function ChatPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ scale: 0.95, opacity: 0 }}
|
initial={{ scale: 0.95, opacity: 0, y: 100 }}
|
||||||
animate={{ scale: 1, opacity: 1 }}
|
animate={{ scale: 1, opacity: 1, y: 0 }}
|
||||||
exit={{ scale: 0.95, opacity: 0 }}
|
exit={{ scale: 0.95, opacity: 0, y: 100 }}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className="w-full max-w-md rounded-3xl shadow-2xl overflow-hidden"
|
className="w-full md:max-w-md rounded-t-3xl md:rounded-3xl shadow-2xl overflow-hidden"
|
||||||
style={{ backgroundColor: 'var(--bg-card)' }}
|
style={{ backgroundColor: 'var(--bg-card)' }}
|
||||||
>
|
>
|
||||||
{/* Modal Header */}
|
{/* Modal Header */}
|
||||||
<div className="p-6 border-b" style={{ borderColor: 'var(--border-color)' }}>
|
<div className="p-4 md:p-6 border-b" style={{ borderColor: 'var(--border-color)' }}>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-3 md:mb-4">
|
||||||
<h2 className="text-2xl font-lora font-semibold" style={{ color: 'var(--text-primary)' }}>
|
<h2 className="text-lg md:text-2xl font-lora font-semibold" style={{ color: 'var(--text-primary)' }}>
|
||||||
Новый чат
|
Новый чат
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
@@ -1279,9 +1280,9 @@ export default function ChatPage() {
|
|||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
setSearchResults([]);
|
setSearchResults([]);
|
||||||
}}
|
}}
|
||||||
className="p-2 rounded-full hover:bg-gray-100 transition"
|
className="p-2 rounded-full hover:bg-gray-100 transition min-h-touch min-w-touch"
|
||||||
>
|
>
|
||||||
<X size={20} style={{ color: 'var(--text-secondary)' }} />
|
<X size={18} className="md:w-5 md:h-5" style={{ color: 'var(--text-secondary)' }} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1292,51 +1293,52 @@ export default function ChatPage() {
|
|||||||
onChange={(e) => handleSearchUsers(e.target.value)}
|
onChange={(e) => handleSearchUsers(e.target.value)}
|
||||||
placeholder="Поиск пользователей..."
|
placeholder="Поиск пользователей..."
|
||||||
autoFocus
|
autoFocus
|
||||||
className="w-full px-4 py-3 rounded-2xl font-inter text-sm outline-none transition"
|
className="w-full px-3 md:px-4 py-2 md:py-3 rounded-lg md:rounded-2xl font-inter text-sm md:text-base outline-none transition"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: 'var(--bg-input)',
|
backgroundColor: 'var(--bg-input)',
|
||||||
color: 'var(--text-primary)',
|
color: 'var(--text-primary)',
|
||||||
borderBottom: '2px solid transparent',
|
borderBottom: '2px solid transparent',
|
||||||
|
fontSize: '16px', // Prevent iOS zoom
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Search Results */}
|
{/* Search Results */}
|
||||||
<div className="max-h-96 overflow-y-auto p-4">
|
<div className="max-h-96 overflow-y-auto p-3 md:p-4">
|
||||||
{searchLoading ? (
|
{searchLoading ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="animate-spin w-8 h-8 mx-auto mb-4 border-2 border-t-transparent rounded-full"
|
<div className="animate-spin w-8 h-8 mx-auto mb-4 border-2 border-t-transparent rounded-full"
|
||||||
style={{ borderColor: 'var(--accent-primary)', borderTopColor: 'transparent' }}></div>
|
style={{ borderColor: 'var(--accent-primary)', borderTopColor: 'transparent' }}></div>
|
||||||
<p className="font-inter text-sm" style={{ color: 'var(--text-secondary)' }}>
|
<p className="font-inter text-xs md:text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
Поиск...
|
Поиск...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : searchQuery.trim().length < 2 ? (
|
) : searchQuery.trim().length < 2 ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<p className="font-inter text-sm" style={{ color: 'var(--text-secondary)' }}>
|
<p className="font-inter text-xs md:text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
Введите имя пользователя для поиска
|
Введите имя пользователя для поиска
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : searchResults.length === 0 ? (
|
) : searchResults.length === 0 ? (
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<p className="font-inter text-sm" style={{ color: 'var(--text-secondary)' }}>
|
<p className="font-inter text-xs md:text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||||
Пользователи не найдены
|
Пользователи не найдены
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-1.5 md:space-y-2">
|
||||||
{searchResults.map((foundUser) => (
|
{searchResults.map((foundUser) => (
|
||||||
<motion.button
|
<motion.button
|
||||||
key={foundUser.id}
|
key={foundUser.id}
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={() => handleStartChatWithUser(foundUser)}
|
onClick={() => handleStartChatWithUser(foundUser)}
|
||||||
className="w-full flex items-center gap-3 p-3 rounded-2xl transition hover:shadow-sm"
|
className="w-full flex items-center gap-2 md:gap-3 p-2.5 md:p-3 rounded-lg md:rounded-2xl transition hover:shadow-sm min-h-touch"
|
||||||
style={{ backgroundColor: 'var(--bg-input)' }}
|
style={{ backgroundColor: 'var(--bg-input)' }}
|
||||||
>
|
>
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<div
|
<div
|
||||||
className="w-12 h-12 flex items-center justify-center flex-shrink-0"
|
className="w-10 h-10 md:w-12 md:h-12 flex items-center justify-center flex-shrink-0"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: foundUser.avatar_url ? `url(${foundUser.avatar_url})` : undefined,
|
backgroundImage: foundUser.avatar_url ? `url(${foundUser.avatar_url})` : undefined,
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
@@ -1351,11 +1353,11 @@ export default function ChatPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User Info */}
|
{/* User Info */}
|
||||||
<div className="flex-1 text-left">
|
<div className="flex-1 text-left min-w-0">
|
||||||
<p className="font-inter font-semibold text-sm" style={{ color: 'var(--text-primary)' }}>
|
<p className="font-inter font-semibold text-xs md:text-sm truncate" style={{ color: 'var(--text-primary)' }}>
|
||||||
{foundUser.display_name || foundUser.username}
|
{foundUser.display_name || foundUser.username}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-inter text-xs" style={{ color: 'var(--text-secondary)' }}>
|
<p className="font-inter text-xs truncate" style={{ color: 'var(--text-secondary)' }}>
|
||||||
@{foundUser.username}
|
@{foundUser.username}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -151,50 +151,51 @@ export default function ProfilePage() {
|
|||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
<div className="w-screen h-screen flex flex-col overflow-hidden" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||||
<div className="max-w-4xl mx-auto p-4 md:p-6">
|
<div className="flex-1 overflow-y-auto flex flex-col">
|
||||||
{/* Header */}
|
<div className="max-w-4xl mx-auto w-full p-3 md:p-6">
|
||||||
<div className="mb-4 md:mb-6 flex items-center justify-between">
|
{/* Header */}
|
||||||
<button
|
<div className="mb-3 md:mb-6 flex items-center justify-between flex-shrink-0">
|
||||||
onClick={() => navigate('/chat')}
|
|
||||||
className="flex items-center gap-2 font-inter font-medium hover:opacity-70 transition"
|
|
||||||
style={{ color: 'var(--accent-primary)' }}
|
|
||||||
>
|
|
||||||
<ArrowLeft size={20} />
|
|
||||||
<span className="hidden sm:inline">Назад к чатам</span>
|
|
||||||
</button>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate('/settings')}
|
onClick={() => navigate('/chat')}
|
||||||
className="flex items-center gap-2 font-inter font-medium hover:opacity-70 transition"
|
className="flex items-center gap-2 font-inter font-medium hover:opacity-70 transition min-h-touch min-w-touch"
|
||||||
style={{ color: 'var(--text-secondary)' }}
|
style={{ color: 'var(--accent-primary)' }}
|
||||||
title="Настройки"
|
|
||||||
>
|
>
|
||||||
<Settings size={20} />
|
<ArrowLeft size={16} className="md:w-5 md:h-5" />
|
||||||
</button>
|
<span className="text-sm md:text-base hidden sm:inline">Назад</span>
|
||||||
<button
|
|
||||||
onClick={handleLogout}
|
|
||||||
className="flex items-center gap-2 font-inter font-medium hover:opacity-70 transition"
|
|
||||||
style={{ color: 'var(--error-color)' }}
|
|
||||||
>
|
|
||||||
<LogOut size={20} />
|
|
||||||
Выйти
|
|
||||||
</button>
|
</button>
|
||||||
|
<div className="flex items-center gap-2 md:gap-3">
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/settings')}
|
||||||
|
className="flex items-center gap-2 font-inter font-medium hover:opacity-70 transition min-h-touch min-w-touch"
|
||||||
|
style={{ color: 'var(--text-secondary)' }}
|
||||||
|
title="Настройки"
|
||||||
|
>
|
||||||
|
<Settings size={16} className="md:w-5 md:h-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="flex items-center gap-1 md:gap-2 font-inter font-medium hover:opacity-70 transition text-xs md:text-sm min-h-touch min-w-touch"
|
||||||
|
style={{ color: 'var(--error-color)' }}
|
||||||
|
>
|
||||||
|
<LogOut size={14} className="md:w-5 md:h-5" />
|
||||||
|
<span className="hidden sm:inline">Выйти</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Verification Banner */}
|
{/* Verification Banner */}
|
||||||
{!user.is_verified && (
|
{!user.is_verified && (
|
||||||
<VerificationBanner userEmail={user.email} />
|
<VerificationBanner userEmail={user.email} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Profile Card */}
|
{/* Profile Card */}
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
className="rounded-2xl md:rounded-3xl shadow-soft p-4 md:p-8"
|
className="rounded-xl md:rounded-3xl shadow-soft p-4 md:p-8"
|
||||||
style={{ backgroundColor: 'var(--bg-card)' }}
|
style={{ backgroundColor: 'var(--bg-card)' }}
|
||||||
>
|
>
|
||||||
{/* Avatar Section */}
|
{/* Avatar Section */}
|
||||||
<div className="flex flex-col items-center mb-6 md:mb-8">
|
<div className="flex flex-col items-center mb-6 md:mb-8">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@@ -572,12 +573,13 @@ export default function ProfilePage() {
|
|||||||
onClick={handleDeleteAccount}
|
onClick={handleDeleteAccount}
|
||||||
whileTap={{ scale: 0.95 }}
|
whileTap={{ scale: 0.95 }}
|
||||||
style={{ backgroundColor: 'var(--error-color)', color: 'white' }}
|
style={{ backgroundColor: 'var(--error-color)', color: 'white' }}
|
||||||
className="py-3 px-6 rounded-full font-inter font-semibold hover:shadow-lg transition-all"
|
className="py-2 md:py-3 px-4 md:px-6 rounded-lg md:rounded-full font-inter font-semibold text-sm md:text-base hover:shadow-lg transition-all min-h-touch"
|
||||||
>
|
>
|
||||||
Удалить аккаунт
|
Удалить аккаунт
|
||||||
</motion.button>
|
</motion.button>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+103
-101
@@ -8,117 +8,119 @@ export default function SettingsPage() {
|
|||||||
const { theme, setTheme } = useThemeStore();
|
const { theme, setTheme } = useThemeStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
<div className="w-screen h-screen flex flex-col overflow-hidden" style={{ backgroundColor: 'var(--bg-primary)' }}>
|
||||||
<div className="max-w-4xl mx-auto p-6">
|
<div className="flex-1 overflow-y-auto flex flex-col">
|
||||||
{/* Header */}
|
<div className="max-w-4xl mx-auto w-full p-3 md:p-6">
|
||||||
<div className="mb-6">
|
{/* Header */}
|
||||||
<button
|
<div className="mb-3 md:mb-6 flex-shrink-0">
|
||||||
onClick={() => navigate('/chat')}
|
<button
|
||||||
className="flex items-center gap-2 font-inter font-medium hover:opacity-70 transition"
|
onClick={() => navigate('/chat')}
|
||||||
style={{ color: 'var(--accent-primary)' }}
|
className="flex items-center gap-2 font-inter font-medium hover:opacity-70 transition min-h-touch min-w-touch text-sm md:text-base"
|
||||||
|
style={{ color: 'var(--accent-primary)' }}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={16} className="md:w-5 md:h-5" />
|
||||||
|
Назад
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Settings Card */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
className="rounded-xl md:rounded-3xl shadow-soft p-4 md:p-8"
|
||||||
|
style={{ backgroundColor: 'var(--bg-card)' }}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<h1 className="text-2xl md:text-3xl font-lora font-semibold mb-6 md:mb-8" style={{ color: 'var(--text-primary)' }}>
|
||||||
Назад к чатам
|
Настройки
|
||||||
</button>
|
</h1>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Settings Card */}
|
{/* Appearance Section */}
|
||||||
<motion.div
|
<div className="space-y-5 md:space-y-6">
|
||||||
initial={{ opacity: 0, y: 20 }}
|
<div>
|
||||||
animate={{ opacity: 1, y: 0 }}
|
<h2 className="text-lg md:text-xl font-lora font-semibold mb-3 md:mb-4" style={{ color: 'var(--text-primary)' }}>
|
||||||
className="rounded-3xl shadow-soft p-8"
|
Внешний вид
|
||||||
style={{ backgroundColor: 'var(--bg-card)' }}
|
</h2>
|
||||||
>
|
|
||||||
<h1 className="text-3xl font-lora font-semibold mb-8" style={{ color: 'var(--text-primary)' }}>
|
|
||||||
Настройки
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Appearance Section */}
|
{/* Theme Selector */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-2 md:space-y-3">
|
||||||
<div>
|
<label className="block font-lora italic text-xs md:text-[15px] mb-2 md:mb-3" style={{ color: 'var(--text-secondary)' }}>
|
||||||
<h2 className="text-xl font-lora font-semibold mb-4" style={{ color: 'var(--text-primary)' }}>
|
Тема оформления
|
||||||
Внешний вид
|
</label>
|
||||||
</h2>
|
|
||||||
|
|
||||||
{/* Theme Selector */}
|
<div className="grid grid-cols-2 gap-2 md:gap-4">
|
||||||
<div className="space-y-3">
|
{/* Light Theme */}
|
||||||
<label className="block font-lora italic text-[15px] mb-3" style={{ color: 'var(--text-secondary)' }}>
|
<motion.button
|
||||||
Тема оформления
|
whileTap={{ scale: 0.98 }}
|
||||||
</label>
|
onClick={() => setTheme('light')}
|
||||||
|
className="relative p-3 md:p-6 rounded-lg md:rounded-2xl border-2 transition-all min-h-touch"
|
||||||
<div className="grid grid-cols-2 gap-4">
|
style={{
|
||||||
{/* Light Theme */}
|
backgroundColor: theme === 'light' ? 'var(--accent-primary-soft)' : 'var(--bg-input)',
|
||||||
<motion.button
|
borderColor: theme === 'light' ? 'var(--accent-primary)' : 'transparent',
|
||||||
whileTap={{ scale: 0.98 }}
|
}}
|
||||||
onClick={() => setTheme('light')}
|
>
|
||||||
className="relative p-6 rounded-2xl border-2 transition-all"
|
<div className="flex flex-col items-center gap-2 md:gap-3">
|
||||||
style={{
|
<div className="w-12 h-12 md:w-16 md:h-16 rounded-full bg-gradient-to-br from-yellow-400 to-orange-500 flex items-center justify-center shadow-lg">
|
||||||
backgroundColor: theme === 'light' ? 'var(--accent-primary-soft)' : 'var(--bg-input)',
|
<Sun size={20} className="md:w-8 md:h-8 text-white" />
|
||||||
borderColor: theme === 'light' ? 'var(--accent-primary)' : 'transparent',
|
</div>
|
||||||
}}
|
<span className="font-inter font-semibold text-xs md:text-sm" style={{ color: 'var(--text-primary)' }}>
|
||||||
>
|
Светлая тема
|
||||||
<div className="flex flex-col items-center gap-3">
|
</span>
|
||||||
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-yellow-400 to-orange-500 flex items-center justify-center shadow-lg">
|
{theme === 'light' && (
|
||||||
<Sun size={32} className="text-white" />
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
className="absolute top-2 md:top-3 right-2 md:right-3 w-5 h-5 md:w-6 md:h-6 rounded-full flex items-center justify-center"
|
||||||
|
style={{ backgroundColor: 'var(--accent-primary)' }}
|
||||||
|
>
|
||||||
|
<svg className="w-3 h-3 md:w-4 md:h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="font-inter font-semibold" style={{ color: 'var(--text-primary)' }}>
|
</motion.button>
|
||||||
Светлая тема
|
|
||||||
</span>
|
|
||||||
{theme === 'light' && (
|
|
||||||
<motion.div
|
|
||||||
initial={{ scale: 0 }}
|
|
||||||
animate={{ scale: 1 }}
|
|
||||||
className="absolute top-3 right-3 w-6 h-6 rounded-full flex items-center justify-center"
|
|
||||||
style={{ backgroundColor: 'var(--accent-primary)' }}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</motion.button>
|
|
||||||
|
|
||||||
{/* Dark Theme */}
|
{/* Dark Theme */}
|
||||||
<motion.button
|
<motion.button
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
onClick={() => setTheme('dark')}
|
onClick={() => setTheme('dark')}
|
||||||
className="relative p-6 rounded-2xl border-2 transition-all"
|
className="relative p-3 md:p-6 rounded-lg md:rounded-2xl border-2 transition-all min-h-touch"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: theme === 'dark' ? 'var(--accent-primary-soft)' : 'var(--bg-input)',
|
backgroundColor: theme === 'dark' ? 'var(--accent-primary-soft)' : 'var(--bg-input)',
|
||||||
borderColor: theme === 'dark' ? 'var(--accent-primary)' : 'transparent',
|
borderColor: theme === 'dark' ? 'var(--accent-primary)' : 'transparent',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-2 md:gap-3">
|
||||||
<div className="w-16 h-16 rounded-full bg-gradient-to-br from-indigo-600 to-purple-700 flex items-center justify-center shadow-lg">
|
<div className="w-12 h-12 md:w-16 md:h-16 rounded-full bg-gradient-to-br from-indigo-600 to-purple-700 flex items-center justify-center shadow-lg">
|
||||||
<Moon size={32} className="text-white" />
|
<Moon size={20} className="md:w-8 md:h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<span className="font-inter font-semibold text-xs md:text-sm" style={{ color: 'var(--text-primary)' }}>
|
||||||
|
Темная тема
|
||||||
|
</span>
|
||||||
|
{theme === 'dark' && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
className="absolute top-2 md:top-3 right-2 md:right-3 w-5 h-5 md:w-6 md:h-6 rounded-full flex items-center justify-center"
|
||||||
|
style={{ backgroundColor: 'var(--accent-primary)' }}
|
||||||
|
>
|
||||||
|
<svg className="w-3 h-3 md:w-4 md:h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="font-inter font-semibold" style={{ color: 'var(--text-primary)' }}>
|
</motion.button>
|
||||||
Темная тема
|
</div>
|
||||||
</span>
|
|
||||||
{theme === 'dark' && (
|
<p className="text-xs md:text-sm font-inter mt-2 md:mt-3" style={{ color: 'var(--text-secondary)' }}>
|
||||||
<motion.div
|
Выберите тему оформления
|
||||||
initial={{ scale: 0 }}
|
</p>
|
||||||
animate={{ scale: 1 }}
|
|
||||||
className="absolute top-3 right-3 w-6 h-6 rounded-full flex items-center justify-center"
|
|
||||||
style={{ backgroundColor: 'var(--accent-primary)' }}
|
|
||||||
>
|
|
||||||
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</motion.button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-sm font-inter mt-3" style={{ color: 'var(--text-secondary)' }}>
|
|
||||||
Выберите тему, которая лучше всего подходит для ваших глаз
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</motion.div>
|
||||||
</motion.div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,29 @@ export default {
|
|||||||
'soft': '0 15px 45px rgba(107, 112, 92, 0.1), 0 0 0 1px rgba(107, 112, 92, 0.05)',
|
'soft': '0 15px 45px rgba(107, 112, 92, 0.1), 0 0 0 1px rgba(107, 112, 92, 0.05)',
|
||||||
'logo': '0 4px 12px rgba(107, 112, 92, 0.15)',
|
'logo': '0 4px 12px rgba(107, 112, 92, 0.15)',
|
||||||
},
|
},
|
||||||
|
spacing: {
|
||||||
|
'safe-start': 'max(1rem, env(safe-area-inset-left))',
|
||||||
|
'safe-end': 'max(1rem, env(safe-area-inset-right))',
|
||||||
|
'safe-top': 'max(1rem, env(safe-area-inset-top))',
|
||||||
|
'safe-bottom': 'max(1rem, env(safe-area-inset-bottom))',
|
||||||
|
},
|
||||||
|
minHeight: {
|
||||||
|
'touch': '44px',
|
||||||
|
'touch-lg': '48px',
|
||||||
|
},
|
||||||
|
minWidth: {
|
||||||
|
'touch': '44px',
|
||||||
|
'touch-lg': '48px',
|
||||||
|
},
|
||||||
|
screens: {
|
||||||
|
'xs': '360px',
|
||||||
|
'sm': '640px',
|
||||||
|
'md': '768px',
|
||||||
|
'lg': '1024px',
|
||||||
|
'xl': '1280px',
|
||||||
|
'2xl': '1536px',
|
||||||
|
'touch': { 'raw': '(hover: none) and (pointer: coarse)' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user