228 lines
9.6 KiB
TypeScript
228 lines
9.6 KiB
TypeScript
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>
|
||
);
|
||
}
|