Files
janichat/frontend/src/components/NewChatModal.tsx

228 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import { X, Search, MessageCircle, Users, Radio } from 'lucide-react';
import api from '../api/client';
import { useStore } from '../store';
import Avatar from './Avatar';
interface Props {
onClose: () => void;
onOpen: (chatId: string) => void;
}
type Step = 'type' | 'user' | 'group';
export default function NewChatModal({ onClose, onOpen }: Props) {
const [step, setStep] = useState<Step>('type');
const [chatType, setChatType] = useState<'private' | 'group' | 'channel'>('private');
const [users, setUsers] = useState<any[]>([]);
const [search, setSearch] = useState('');
const [selected, setSelected] = useState<string[]>([]);
const [groupTitle, setGroupTitle] = useState('');
const [loading, setLoading] = useState(false);
const { setChats, chats } = useStore();
useEffect(() => {
api.get('/api/users').then(r => setUsers(r.data));
}, []);
const filtered = users.filter(u =>
u.displayName.toLowerCase().includes(search.toLowerCase()) ||
u.username.toLowerCase().includes(search.toLowerCase())
);
function toggleSelect(id: string) {
setSelected(s => s.includes(id) ? s.filter(x => x !== id) : [...s, id]);
}
async function createPrivate(userId: string) {
setLoading(true);
try {
const { data } = await api.post(`/api/chats/private/${userId}`);
const { data: chat } = await api.get(`/api/chats/${data.id}`);
// Refresh chats
const { data: allChats } = await api.get('/api/chats');
setChats(allChats);
onOpen(data.id);
onClose();
} finally {
setLoading(false);
}
}
async function createGroup() {
if (!groupTitle.trim()) return;
setLoading(true);
try {
const { data } = await api.post('/api/chats', {
type: chatType,
title: groupTitle,
memberIds: selected,
});
const { data: allChats } = await api.get('/api/chats');
setChats(allChats);
onOpen(data.id);
onClose();
} finally {
setLoading(false);
}
}
return (
<div className="fixed inset-0 bg-black/50 flex items-end sm:items-center justify-center z-50 p-0 sm:p-4">
<div className="bg-white rounded-t-2xl sm:rounded-2xl w-full sm:max-w-md max-h-[85vh] flex flex-col">
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-gray-100">
<h2 className="font-semibold text-gray-900">
{step === 'type' ? 'Новый чат' : step === 'user' ? 'Выбор пользователя' : 'Создание ' + (chatType === 'channel' ? 'канала' : 'группы')}
</h2>
<button onClick={onClose} className="p-1 rounded-lg hover:bg-gray-100 text-gray-400">
<X className="w-5 h-5" />
</button>
</div>
{/* Step 1: Type selection */}
{step === 'type' && (
<div className="p-5 space-y-3">
<button
onClick={() => { setChatType('private'); setStep('user'); }}
className="w-full flex items-center gap-4 p-4 rounded-xl hover:bg-blue-50 border border-gray-100 transition-colors"
>
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center">
<MessageCircle className="w-5 h-5 text-blue-600" />
</div>
<div className="text-left">
<div className="font-medium text-gray-900">Личный чат</div>
<div className="text-sm text-gray-500">Переписка с одним пользователем</div>
</div>
</button>
<button
onClick={() => { setChatType('group'); setStep('group'); }}
className="w-full flex items-center gap-4 p-4 rounded-xl hover:bg-purple-50 border border-gray-100 transition-colors"
>
<div className="w-10 h-10 bg-purple-100 rounded-full flex items-center justify-center">
<Users className="w-5 h-5 text-purple-600" />
</div>
<div className="text-left">
<div className="font-medium text-gray-900">Группа</div>
<div className="text-sm text-gray-500">Общение нескольких участников</div>
</div>
</button>
<button
onClick={() => { setChatType('channel'); setStep('group'); }}
className="w-full flex items-center gap-4 p-4 rounded-xl hover:bg-orange-50 border border-gray-100 transition-colors"
>
<div className="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center">
<Radio className="w-5 h-5 text-orange-600" />
</div>
<div className="text-left">
<div className="font-medium text-gray-900">Канал</div>
<div className="text-sm text-gray-500">Публикации от администраторов</div>
</div>
</button>
</div>
)}
{/* Step 2: Select user (private) */}
{step === 'user' && (
<>
<div className="px-4 py-3 border-b border-gray-100">
<div className="flex items-center gap-2 bg-gray-100 rounded-xl px-3 py-2">
<Search className="w-4 h-4 text-gray-400" />
<input
value={search} onChange={e => setSearch(e.target.value)}
placeholder="Поиск..." className="flex-1 bg-transparent text-sm outline-none"
autoFocus
/>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-1">
{filtered.map(u => (
<button
key={u.id}
onClick={() => createPrivate(u.id)}
disabled={loading}
className="w-full flex items-center gap-3 px-3 py-2.5 rounded-xl hover:bg-gray-50 text-left"
>
<Avatar name={u.displayName} color={u.avatarColor} online={u.online} />
<div>
<div className="font-medium text-sm text-gray-900">{u.displayName}</div>
<div className="text-xs text-gray-400">@{u.username}</div>
</div>
</button>
))}
</div>
</>
)}
{/* Step 3: Create group/channel */}
{step === 'group' && (
<>
<div className="px-4 py-3 border-b border-gray-100 space-y-3">
<input
value={groupTitle}
onChange={e => setGroupTitle(e.target.value)}
placeholder={chatType === 'channel' ? 'Название канала' : 'Название группы'}
className="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-200"
autoFocus
/>
<div className="flex items-center gap-2 bg-gray-100 rounded-xl px-3 py-2">
<Search className="w-4 h-4 text-gray-400" />
<input
value={search} onChange={e => setSearch(e.target.value)}
placeholder="Добавить участников..." className="flex-1 bg-transparent text-sm outline-none"
/>
</div>
{selected.length > 0 && (
<div className="flex flex-wrap gap-1">
{selected.map(id => {
const u = users.find(x => x.id === id);
return u ? (
<span key={id} onClick={() => toggleSelect(id)}
className="flex items-center gap-1 bg-blue-100 text-blue-700 text-xs px-2 py-1 rounded-full cursor-pointer">
{u.displayName} ×
</span>
) : null;
})}
</div>
)}
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-1">
{filtered.map(u => (
<button
key={u.id}
onClick={() => toggleSelect(u.id)}
className={`w-full flex items-center gap-3 px-3 py-2.5 rounded-xl text-left transition-colors ${
selected.includes(u.id) ? 'bg-blue-50' : 'hover:bg-gray-50'
}`}
>
<div className="relative">
<Avatar name={u.displayName} color={u.avatarColor} />
{selected.includes(u.id) && (
<div className="absolute -bottom-0.5 -right-0.5 w-4 h-4 bg-blue-500 rounded-full flex items-center justify-center">
<span className="text-white text-[10px]"></span>
</div>
)}
</div>
<div>
<div className="font-medium text-sm text-gray-900">{u.displayName}</div>
<div className="text-xs text-gray-400">@{u.username}</div>
</div>
</button>
))}
</div>
<div className="p-4 border-t border-gray-100">
<button
onClick={createGroup}
disabled={!groupTitle.trim() || loading}
className="w-full bg-blue-500 hover:bg-blue-600 disabled:opacity-40 text-white py-3 rounded-xl font-medium text-sm transition-colors"
>
{loading ? 'Создание...' : `Создать ${chatType === 'channel' ? 'канал' : 'группу'}`}
</button>
</div>
</>
)}
</div>
</div>
);
}