Новый дизайн, удаление
This commit is contained in:
@@ -16,7 +16,7 @@ export default function App() {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center bg-surface">
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<VortexLoader />
|
||||
<AppLoader />
|
||||
<p className="text-zinc-500 text-sm">Загрузка...</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,16 +37,16 @@ export default function App() {
|
||||
);
|
||||
}
|
||||
|
||||
function VortexLoader() {
|
||||
function AppLoader() {
|
||||
return (
|
||||
<div className="relative w-12 h-12">
|
||||
<div className="absolute inset-0 rounded-full border-2 border-transparent border-t-vortex-500 animate-spin" />
|
||||
<div className="absolute inset-0 rounded-full border-2 border-transparent border-t-accent animate-spin" />
|
||||
<div
|
||||
className="absolute inset-1 rounded-full border-2 border-transparent border-t-vortex-400 animate-spin"
|
||||
className="absolute inset-1 rounded-full border-2 border-transparent border-t-accent/70 animate-spin"
|
||||
style={{ animationDuration: '0.8s', animationDirection: 'reverse' }}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-2 rounded-full border-2 border-transparent border-t-vortex-300 animate-spin"
|
||||
className="absolute inset-2 rounded-full border-2 border-transparent border-t-accent/40 animate-spin"
|
||||
style={{ animationDuration: '0.6s' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Forward,
|
||||
Bookmark,
|
||||
ArrowLeft,
|
||||
MessageSquare,
|
||||
} from 'lucide-react';
|
||||
import { useChatStore } from '../stores/chatStore';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
@@ -317,10 +318,10 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
|
||||
if (!activeChat || !chat) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center bg-surface-secondary/50 rounded-[2rem] overflow-hidden border border-white/5 shadow-2xl relative z-0 backdrop-blur-3xl group">
|
||||
<div className="flex-1 h-full flex items-center justify-center bg-surface-secondary/50 rounded-[2rem] overflow-hidden border border-white/5 shadow-2xl relative z-0 backdrop-blur-3xl group">
|
||||
{/* Slowly pulsing purple background as requested */}
|
||||
<div className="absolute inset-0 pointer-events-none overflow-hidden transition-opacity duration-[10000ms]">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[60vw] h-[60vw] max-w-[800px] max-h-[800px] bg-vortex-600/10 rounded-full blur-[120px] animate-[pulse_8s_ease-in-out_infinite]" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[60vw] h-[60vw] max-w-[800px] max-h-[800px] bg-accent/10 rounded-full blur-[120px] animate-[pulse_8s_ease-in-out_infinite]" />
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[40vw] h-[40vw] max-w-[500px] max-h-[500px] bg-purple-600/15 rounded-full blur-[100px] animate-[pulse_12s_ease-in-out_infinite_reverse]" />
|
||||
</div>
|
||||
|
||||
@@ -331,18 +332,18 @@ export default function ChatView({ onStartCall, onStartGroupCall }: { onStartCal
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="w-28 h-28 mx-auto mb-8 rounded-[2rem] bg-gradient-to-br from-vortex-500/20 to-purple-600/20 flex items-center justify-center shadow-[0_0_60px_-15px_var(--color-accent)] ring-1 ring-white/10 backdrop-blur-2xl relative"
|
||||
className="w-28 h-28 mx-auto mb-8 rounded-[2rem] bg-gradient-to-br from-accent/20 to-purple-600/20 flex items-center justify-center shadow-[0_0_60px_-15px_var(--color-accent)] ring-1 ring-white/10 backdrop-blur-2xl relative"
|
||||
>
|
||||
<div className="absolute inset-0 rounded-[2rem] bg-gradient-to-br from-white/[0.05] to-transparent pointer-events-none" />
|
||||
<img src="/logo.png" alt="Vortex" className="w-16 h-16 rounded-2xl object-cover shadow-2xl transform hover:scale-105 transition-transform" />
|
||||
<MessageSquare className="w-16 h-16 text-accent shadow-2xl transform hover:scale-105 transition-transform" />
|
||||
</motion.div>
|
||||
<motion.h2
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ duration: 0.6, delay: 0.1, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-vortex-400 via-fuchsia-400 to-indigo-400 mb-4 drop-shadow-lg tracking-tight"
|
||||
className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-accent via-fuchsia-400 to-indigo-400 mb-4 drop-shadow-lg tracking-tight"
|
||||
>
|
||||
Vortex Messenger
|
||||
SelfHost Messenger
|
||||
</motion.h2>
|
||||
<motion.p
|
||||
initial={{ y: 20, opacity: 0 }}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { Phone, PhoneOff, Video, VideoOff, Mic, MicOff, Monitor, MonitorOff, Minimize2, Volume2, ShieldCheck, ShieldOff, ChevronUp } from 'lucide-react';
|
||||
import { useChatStore } from '../stores/chatStore';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { getSocket } from '../lib/socket';
|
||||
import { api } from '../lib/api';
|
||||
import { useLang } from '../lib/i18n';
|
||||
@@ -10,6 +12,7 @@ interface ParticipantInfo {
|
||||
username: string;
|
||||
displayName?: string;
|
||||
avatar?: string | null;
|
||||
isSharingScreen?: boolean;
|
||||
}
|
||||
|
||||
interface PeerState {
|
||||
@@ -65,6 +68,7 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
const [microphones, setMicrophones] = useState<MediaDeviceInfo[]>([]);
|
||||
const [activeMicId, setActiveMicId] = useState<string>('');
|
||||
const [showMicMenu, setShowMicMenu] = useState(false);
|
||||
const [sharingUserId, setSharingUserId] = useState<string | null>(null);
|
||||
|
||||
const localStreamRef = useRef<MediaStream | null>(null);
|
||||
const screenStreamRef = useRef<MediaStream | null>(null);
|
||||
@@ -289,6 +293,8 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
}
|
||||
}
|
||||
setIsScreenSharing(false);
|
||||
const socket = getSocket();
|
||||
socket?.emit('screen_share_stopped', chatId);
|
||||
} else {
|
||||
try {
|
||||
const screenStream = await navigator.mediaDevices.getDisplayMedia({
|
||||
@@ -328,6 +334,8 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
};
|
||||
|
||||
setIsScreenSharing(true);
|
||||
const socket = getSocket();
|
||||
socket?.emit('screen_share_started', chatId);
|
||||
} catch { console.error('Screen share failed'); }
|
||||
}
|
||||
}, [isScreenSharing, chatId]);
|
||||
@@ -521,6 +529,9 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
const newMap = new Map<string, ParticipantInfo>();
|
||||
for (const p of data.participants) {
|
||||
newMap.set(p.id, p);
|
||||
if (p.isSharingScreen && p.id !== useAuthStore.getState().user?.id) {
|
||||
setSharingUserId(p.id);
|
||||
}
|
||||
// Create peer connection to each existing participant (we are the initiator)
|
||||
if (!peersRef.current.has(p.id)) {
|
||||
await createPeerConnection(p.id, true);
|
||||
@@ -605,6 +616,38 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
}
|
||||
};
|
||||
|
||||
const onScreenShareStarted = (data: { userId: string }) => {
|
||||
const isMe = data.userId === useAuthStore.getState().user?.id;
|
||||
if (!isMe) {
|
||||
setSharingUserId(data.userId);
|
||||
}
|
||||
setParticipants(prev => {
|
||||
const next = new Map(prev);
|
||||
const p = next.get(data.userId);
|
||||
if (p) next.set(data.userId, { ...p, isSharingScreen: true });
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const onScreenShareStopped = (data: { userId: string }) => {
|
||||
const isMe = data.userId === useAuthStore.getState().user?.id;
|
||||
if (!isMe && sharingUserId === data.userId) {
|
||||
setSharingUserId(null);
|
||||
} else if (isMe) {
|
||||
// Just in case
|
||||
}
|
||||
|
||||
// If the person who stopped was the sharing user, clear it
|
||||
setSharingUserId(current => current === data.userId ? null : current);
|
||||
|
||||
setParticipants(prev => {
|
||||
const next = new Map(prev);
|
||||
const p = next.get(data.userId);
|
||||
if (p) next.set(data.userId, { ...p, isSharingScreen: false });
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
socket.on('group_call_participants', onParticipants);
|
||||
socket.on('group_call_user_joined', onUserJoined);
|
||||
socket.on('group_call_user_left', onUserLeft);
|
||||
@@ -613,6 +656,8 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
socket.on('group_ice_candidate', onIceCandidate);
|
||||
socket.on('group_call_renegotiate', onRenegotiate);
|
||||
socket.on('group_call_renegotiate_answer', onRenegotiateAnswer);
|
||||
socket.on('screen_share_started', onScreenShareStarted);
|
||||
socket.on('screen_share_stopped', onScreenShareStopped);
|
||||
|
||||
return () => {
|
||||
socket.off('group_call_participants', onParticipants);
|
||||
@@ -623,6 +668,8 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
socket.off('group_ice_candidate', onIceCandidate);
|
||||
socket.off('group_call_renegotiate', onRenegotiate);
|
||||
socket.off('group_call_renegotiate_answer', onRenegotiateAnswer);
|
||||
socket.off('screen_share_started', onScreenShareStarted);
|
||||
socket.off('screen_share_stopped', onScreenShareStopped);
|
||||
};
|
||||
}, [isOpen, chatId, createPeerConnection]);
|
||||
|
||||
@@ -856,6 +903,16 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Screen share indicator */}
|
||||
{sharingUserId && (
|
||||
<div className="absolute top-6 left-6 z-20 flex items-center gap-2 px-3 py-1.5 rounded-full bg-blue-500/20 border border-blue-500/30 backdrop-blur-md">
|
||||
<Monitor size={14} className="text-blue-400 animate-pulse" />
|
||||
<span className="text-xs font-medium text-blue-100">
|
||||
{participants.get(sharingUserId)?.displayName || 'Кто-то'} транслирует экран
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Controls */}
|
||||
<div className="px-8 pb-8 pt-4 flex items-center justify-center gap-4 flex-wrap">
|
||||
{/* Mic with dropdown */}
|
||||
@@ -882,12 +939,16 @@ export default function GroupCallModal({ isOpen, onClose, chatId, chatName, call
|
||||
{isVideoOff ? <VideoOff size={18} /> : <Video size={18} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleScreenShare}
|
||||
className={`w-11 h-11 rounded-full flex items-center justify-center transition-colors ${isScreenSharing ? 'bg-vortex-500/30 text-vortex-400' : 'bg-white/10 text-white hover:bg-white/20'}`}
|
||||
title={isScreenSharing ? t('stopScreenShare') : t('screenShare')}
|
||||
>
|
||||
{isScreenSharing ? <MonitorOff size={18} /> : <Monitor size={18} />}
|
||||
</button>
|
||||
onClick={toggleScreenShare}
|
||||
disabled={!!sharingUserId && !isScreenSharing}
|
||||
className={`w-12 h-12 rounded-full flex items-center justify-center transition-all ${isScreenSharing
|
||||
? 'bg-blue-500 text-white shadow-lg shadow-blue-500/40'
|
||||
: 'bg-white/10 text-white hover:bg-white/15'
|
||||
} ${!!sharingUserId && !isScreenSharing ? 'opacity-30 cursor-not-allowed' : ''}`}
|
||||
title={sharingUserId ? 'Кто-то уже транслирует экран' : ''}
|
||||
>
|
||||
{isScreenSharing ? <MonitorOff size={20} /> : <Monitor size={20} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={e => { e.stopPropagation(); setShowVolumeSlider(!showVolumeSlider); }}
|
||||
className="w-11 h-11 rounded-full flex items-center justify-center transition-colors bg-white/10 text-white hover:bg-white/20"
|
||||
|
||||
@@ -55,6 +55,7 @@ export default function NewChatModal({ onClose }: NewChatModalProps) {
|
||||
try {
|
||||
const chat = await api.createPersonalChat(selectedUser.id);
|
||||
addChat(chat);
|
||||
import('../lib/socket').then(({ getSocket }) => getSocket()?.emit('join_chat', chat.id));
|
||||
setActiveChat(chat.id);
|
||||
loadMessages(chat.id);
|
||||
onClose();
|
||||
@@ -75,14 +76,15 @@ export default function NewChatModal({ onClose }: NewChatModalProps) {
|
||||
if (!groupName.trim() || selectedUsers.length === 0) return;
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const chat = await api.createGroupChat(
|
||||
groupName.trim(),
|
||||
selectedUsers.map((u) => u.id)
|
||||
);
|
||||
addChat(chat);
|
||||
setActiveChat(chat.id);
|
||||
loadMessages(chat.id);
|
||||
onClose();
|
||||
const chat = await api.createGroupChat(
|
||||
groupName.trim(),
|
||||
selectedUsers.map((u) => u.id)
|
||||
);
|
||||
addChat(chat);
|
||||
import('../lib/socket').then(({ getSocket }) => getSocket()?.emit('join_chat', chat.id));
|
||||
setActiveChat(chat.id);
|
||||
loadMessages(chat.id);
|
||||
onClose();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
|
||||
@@ -303,7 +303,7 @@ export default function SideMenu({ isOpen, onClose }: SideMenuProps) {
|
||||
{ icon: Users, label: t('friends'), onClick: () => changeView('friends'), badge: friendRequests.length > 0 ? friendRequests.length : undefined },
|
||||
{ icon: Settings, label: t('settings'), onClick: () => changeView('settings') },
|
||||
{ divider: true },
|
||||
{ icon: Info, label: t('aboutApp'), subtitle: 'Vortex Messenger v1.0', onClick: () => changeView('about') },
|
||||
{ icon: Info, label: t('aboutApp'), subtitle: 'SelfHost Messenger v1.0', onClick: () => changeView('about') },
|
||||
];
|
||||
|
||||
// Slide direction for animations
|
||||
@@ -664,13 +664,13 @@ export default function SideMenu({ isOpen, onClose }: SideMenuProps) {
|
||||
</div>
|
||||
<div className="px-5 py-3">
|
||||
<h4 className="text-xs text-zinc-500 uppercase tracking-wide mb-3">{t('about')}</h4>
|
||||
<div className="flex items-center gap-4 px-3 py-3 rounded-xl bg-surface-tertiary/50">
|
||||
<Info size={18} className="text-zinc-400" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-zinc-200">Vortex Messenger</p>
|
||||
<p className="text-xs text-zinc-500">{t('version')} 1.0.0</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 px-3 py-3 rounded-xl bg-surface-tertiary/50">
|
||||
<Info size={18} className="text-zinc-400" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm text-zinc-200">SelfHost Messenger</p>
|
||||
<p className="text-xs text-zinc-500">{t('version')} 1.0.0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
@@ -86,8 +86,10 @@ export default function Sidebar() {
|
||||
<Menu size={20} />
|
||||
</button>
|
||||
<div className="flex items-center gap-2 flex-1 min-w-0">
|
||||
<img src="/logo.png" alt="Vortex" className="w-8 h-8 rounded-lg object-cover" />
|
||||
<h1 className="text-lg font-bold gradient-text truncate">Vortex</h1>
|
||||
<div className="w-8 h-8 rounded-lg bg-accent flex items-center justify-center text-white">
|
||||
<MessageSquare size={18} />
|
||||
</div>
|
||||
<h1 className="text-lg font-bold gradient-text truncate">SelfHost</h1>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowNewChat(true)}
|
||||
@@ -128,8 +130,8 @@ export default function Sidebar() {
|
||||
onClick={() => setShowCreateStory(true)}
|
||||
className="flex flex-col items-center gap-1 flex-shrink-0 group"
|
||||
>
|
||||
<div className="w-12 h-12 sm:w-14 sm:h-14 rounded-full border-2 border-dashed border-zinc-600 flex items-center justify-center group-hover:border-vortex-400 transition-colors">
|
||||
<Plus size={18} className="text-zinc-400 group-hover:text-vortex-400 transition-colors" />
|
||||
<div className="w-12 h-12 sm:w-14 sm:h-14 rounded-full border-2 border-dashed border-zinc-600 flex items-center justify-center group-hover:border-accent transition-colors">
|
||||
<Plus size={18} className="text-zinc-400 group-hover:text-accent transition-colors" />
|
||||
</div>
|
||||
<span className="text-[10px] text-zinc-500 truncate w-12 sm:w-14 text-center">{t('newStory')}</span>
|
||||
</button>
|
||||
@@ -145,7 +147,7 @@ export default function Sidebar() {
|
||||
>
|
||||
<div className={`w-14 h-14 rounded-full p-[2.5px] transition-transform group-hover:scale-105 ${
|
||||
group.hasUnviewed
|
||||
? 'bg-gradient-to-tr from-vortex-400 via-purple-500 to-pink-500 shadow-lg shadow-vortex-500/25'
|
||||
? 'bg-gradient-to-tr from-accent via-purple-500 to-pink-500 shadow-lg shadow-accent/25'
|
||||
: isMine
|
||||
? 'bg-gradient-to-tr from-zinc-500 to-zinc-600'
|
||||
: 'bg-zinc-700'
|
||||
|
||||
@@ -3,6 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { X, ChevronLeft, ChevronRight, Eye, Trash2, Plus, ChevronUp } from 'lucide-react';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { api } from '../lib/api';
|
||||
import { getSocket } from '../lib/socket';
|
||||
import { useLang } from '../lib/i18n';
|
||||
import { getInitials, generateAvatarColor } from '../lib/utils';
|
||||
import Avatar from './Avatar';
|
||||
@@ -124,16 +125,49 @@ export default function StoryViewer({ stories, initialUserIndex, onClose, onRefr
|
||||
};
|
||||
}, [storyIndex, userIndex, paused, goNext]);
|
||||
|
||||
// Keyboard
|
||||
// Keyboard and Socket
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
if (e.key === 'ArrowRight') goNext();
|
||||
if (e.key === 'ArrowLeft') goPrev();
|
||||
};
|
||||
|
||||
const socket = getSocket();
|
||||
const handleStoryViewed = (data: { storyId: string; userId: string; username: string; displayName: string; avatar: string | null; viewedAt: string; viewCount: number }) => {
|
||||
if (!currentStory || data.storyId !== currentStory.id) return;
|
||||
|
||||
setViewOverrides(prev => ({
|
||||
...prev,
|
||||
[data.storyId]: {
|
||||
viewCount: data.viewCount,
|
||||
viewed: prev[data.storyId]?.viewed || false
|
||||
}
|
||||
}));
|
||||
|
||||
// Update viewers list if visible
|
||||
if (showViewers) {
|
||||
setViewers(prev => {
|
||||
if (prev.some(v => v.userId === data.userId)) return prev;
|
||||
return [...prev, {
|
||||
userId: data.userId,
|
||||
username: data.username,
|
||||
displayName: data.displayName,
|
||||
avatar: data.avatar,
|
||||
viewedAt: data.viewedAt
|
||||
}].sort((a, b) => new Date(b.viewedAt).getTime() - new Date(a.viewedAt).getTime());
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [goNext, goPrev, onClose]);
|
||||
socket?.on('story_viewed', handleStoryViewed);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('keydown', onKey);
|
||||
socket?.off('story_viewed', handleStoryViewed);
|
||||
};
|
||||
}, [goNext, goPrev, onClose, currentStory?.id, showViewers]);
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!currentStory) return;
|
||||
|
||||
@@ -170,7 +170,7 @@ const translations = {
|
||||
removeAvatar: 'Удалить аватар',
|
||||
namePlaceholder: 'Имя',
|
||||
notSpecified: 'Не указано',
|
||||
onVortexSince: 'На Vortex с',
|
||||
onVortexSince: 'На SelfHost с',
|
||||
cancel: 'Отмена',
|
||||
confirm: 'Подтвердить',
|
||||
save: 'Сохранить',
|
||||
@@ -292,7 +292,7 @@ const translations = {
|
||||
version: 'Version',
|
||||
modernMessenger: 'A modern messenger focused',
|
||||
onPrivacy: 'on privacy and convenience',
|
||||
modernMessengerShort: 'Modern messenger',
|
||||
modernMessengerShort: 'SelfHost Messenger',
|
||||
searchChats: 'Search chats...',
|
||||
chat: 'Chat',
|
||||
group: 'Group',
|
||||
@@ -425,7 +425,7 @@ const translations = {
|
||||
removeAvatar: 'Remove avatar',
|
||||
namePlaceholder: 'Name',
|
||||
notSpecified: 'Not specified',
|
||||
onVortexSince: 'On Vortex since',
|
||||
onVortexSince: 'On SelfHost since',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Confirm',
|
||||
save: 'Save',
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState, FormEvent } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useAuthStore } from '../stores/authStore';
|
||||
import { useLang } from '../lib/i18n';
|
||||
import { Eye, EyeOff, ArrowRight, UserPlus, LogIn } from 'lucide-react';
|
||||
import { Eye, EyeOff, ArrowRight, UserPlus, LogIn, MessageSquare } from 'lucide-react';
|
||||
|
||||
export default function AuthPage() {
|
||||
const [isLogin, setIsLogin] = useState(true);
|
||||
@@ -44,9 +44,9 @@ export default function AuthPage() {
|
||||
{/* Анимированный фон */}
|
||||
<div className="absolute inset-0 pointer-events-none">
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[800px] h-[800px] opacity-20">
|
||||
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-vortex-600/30 to-purple-600/30 blur-[120px] animate-pulse" />
|
||||
<div className="absolute inset-0 rounded-full bg-gradient-to-r from-accent/30 to-purple-600/30 blur-[120px] animate-pulse" />
|
||||
</div>
|
||||
<div className="absolute top-20 left-20 w-72 h-72 bg-vortex-500/10 rounded-full blur-[100px]" />
|
||||
<div className="absolute top-20 left-20 w-72 h-72 bg-accent/10 rounded-full blur-[100px]" />
|
||||
<div className="absolute bottom-20 right-20 w-96 h-96 bg-purple-500/10 rounded-full blur-[100px]" />
|
||||
</div>
|
||||
|
||||
@@ -57,7 +57,7 @@ export default function AuthPage() {
|
||||
transition={{ duration: 0.4, ease: 'easeOut' }}
|
||||
className="relative z-10 w-full max-w-md mx-4"
|
||||
>
|
||||
<div className="glass-strong rounded-3xl p-8 shadow-2xl shadow-vortex-500/5">
|
||||
<div className="glass-strong rounded-3xl p-8 shadow-2xl shadow-accent/5">
|
||||
|
||||
{/* Заголовок */}
|
||||
<div className="flex flex-col items-center mb-8">
|
||||
@@ -65,14 +65,11 @@ export default function AuthPage() {
|
||||
initial={{ rotate: -180, scale: 0 }}
|
||||
animate={{ rotate: 0, scale: 1 }}
|
||||
transition={{ duration: 0.6, type: 'spring', bounce: 0.4 }}
|
||||
className="w-20 h-20 rounded-2xl bg-accent flex items-center justify-center text-white shadow-lg shadow-accent/30"
|
||||
>
|
||||
<img
|
||||
src="/logo.png"
|
||||
alt="Vortex"
|
||||
className="w-20 h-20 rounded-2xl shadow-lg shadow-vortex-500/30 object-cover"
|
||||
/>
|
||||
<MessageSquare size={40} />
|
||||
</motion.div>
|
||||
<h1 className="text-2xl font-bold gradient-text mt-4">Vortex</h1>
|
||||
<h1 className="text-2xl font-bold gradient-text mt-4">SelfHost</h1>
|
||||
<p className="text-zinc-500 text-sm mt-1">
|
||||
{isLogin ? t('login') : t('register')}
|
||||
</p>
|
||||
@@ -104,7 +101,7 @@ export default function AuthPage() {
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value.replace(/[^a-zA-Z0-9_]/g, ''))}
|
||||
placeholder="username"
|
||||
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all outline-hidden"
|
||||
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-accent/50 focus:ring-1 focus:ring-accent/25 transition-all outline-hidden"
|
||||
required
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
@@ -144,7 +141,7 @@ export default function AuthPage() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder={t('passwordPlaceholder')}
|
||||
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all pr-12 outline-hidden"
|
||||
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-accent/50 focus:ring-1 focus:ring-accent/25 transition-all pr-12 outline-hidden"
|
||||
required
|
||||
autoComplete={isLogin ? 'current-password' : 'new-password'}
|
||||
minLength={8}
|
||||
@@ -159,7 +156,7 @@ export default function AuthPage() {
|
||||
</div>
|
||||
{!isLogin && (
|
||||
<p className="mt-1.5 text-xs text-zinc-500 flex items-center gap-1.5">
|
||||
<div className="w-1 h-1 rounded-full bg-vortex-500" />
|
||||
<div className="w-1 h-1 rounded-full bg-accent" />
|
||||
{t('passwordRequirements')}
|
||||
</p>
|
||||
)}
|
||||
@@ -179,7 +176,7 @@ export default function AuthPage() {
|
||||
value={bio}
|
||||
onChange={(e) => setBio(e.target.value)}
|
||||
placeholder={t('bioPlaceholder')}
|
||||
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-vortex-500/50 focus:ring-1 focus:ring-vortex-500/25 transition-all outline-hidden"
|
||||
className="w-full px-4 py-3 rounded-xl bg-white/5 border border-white/10 text-white placeholder-zinc-600 focus:border-accent/50 focus:ring-1 focus:ring-accent/25 transition-all outline-hidden"
|
||||
/>
|
||||
</motion.div>
|
||||
)}
|
||||
@@ -190,7 +187,7 @@ export default function AuthPage() {
|
||||
whileTap={{ scale: 0.99 }}
|
||||
disabled={isSubmitting}
|
||||
type="submit"
|
||||
className="w-full py-3.5 px-4 rounded-xl bg-gradient-to-r from-vortex-500 to-purple-600 text-white font-medium shadow-lg shadow-vortex-500/25 hover:shadow-vortex-500/40 transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed mt-2"
|
||||
className="w-full py-3.5 px-4 rounded-xl bg-gradient-to-r from-accent to-purple-600 text-white font-medium shadow-lg shadow-accent/25 hover:shadow-accent/40 transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed mt-2"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
@@ -215,7 +212,7 @@ export default function AuthPage() {
|
||||
setDisplayName('');
|
||||
setBio('');
|
||||
}}
|
||||
className="text-vortex-400 hover:text-vortex-300 font-medium transition-colors ml-1"
|
||||
className="text-accent hover:text-accent font-medium transition-colors ml-1"
|
||||
>
|
||||
{isLogin ? t('registerNow') : t('loginNow')}
|
||||
</button>
|
||||
|
||||
@@ -30,6 +30,7 @@ export default function ChatPage() {
|
||||
setPinnedMessage,
|
||||
removePinnedMessage,
|
||||
clearStore,
|
||||
addChat,
|
||||
} = useChatStore();
|
||||
const { user } = useAuthStore();
|
||||
const initialized = useRef(false);
|
||||
@@ -134,6 +135,11 @@ export default function ChatPage() {
|
||||
updateMessage(message);
|
||||
});
|
||||
|
||||
socket.on('new_chat', (chat: any) => {
|
||||
addChat(chat);
|
||||
socket.emit('join_chat', chat.id);
|
||||
});
|
||||
|
||||
socket.on('message_deleted', (data: { messageId: string; chatId: string }) => {
|
||||
removeMessage(data.messageId, data.chatId);
|
||||
});
|
||||
@@ -215,6 +221,7 @@ export default function ChatPage() {
|
||||
socket.off('new_message');
|
||||
socket.off('scheduled_delivered');
|
||||
socket.off('message_edited');
|
||||
socket.off('new_chat');
|
||||
socket.off('message_deleted');
|
||||
socket.off('messages_deleted');
|
||||
socket.off('messages_hidden');
|
||||
|
||||
@@ -435,7 +435,12 @@ export const useChatStore = create<ChatState>((set, get) => ({
|
||||
|
||||
addChat: (chat) => {
|
||||
set((state) => {
|
||||
if (state.chats.some((c) => c.id === chat.id)) return state;
|
||||
const existing = state.chats.find((c) => c.id === chat.id);
|
||||
if (existing) {
|
||||
return {
|
||||
chats: state.chats.map((c) => (c.id === chat.id ? { ...c, ...chat } : c)),
|
||||
};
|
||||
}
|
||||
return { chats: [chat, ...state.chats] };
|
||||
});
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user