Merge pull request #20 from lorsanstand/dev

mobile optimization
This commit is contained in:
Станислав
2026-02-24 21:26:18 +03:00
committed by GitHub
9 changed files with 714 additions and 268 deletions
+186
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
} }
+9 -9
View File
@@ -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
View File
@@ -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>
+41 -39
View File
@@ -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
View File
@@ -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>
); );
+23
View File
@@ -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: [],