fix and update
This commit is contained in:
141
apps/frontend/components/family/family-invite-dialog.tsx
Normal file
141
apps/frontend/components/family/family-invite-dialog.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { AvatarWithPresence } from '@/components/family/avatar-with-presence';
|
||||
import { useAuth } from '@/components/id/auth-provider';
|
||||
import { useToast } from '@/components/id/toast-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { FamilyInviteCandidate, getApiErrorMessage, searchFamilyInviteUsers, sendFamilyInvite } from '@/lib/api';
|
||||
|
||||
export function FamilyInviteDialog({
|
||||
groupId,
|
||||
open,
|
||||
onOpenChange,
|
||||
onInvited
|
||||
}: {
|
||||
groupId: string;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onInvited?: () => void;
|
||||
}) {
|
||||
const { token } = useAuth();
|
||||
const { showToast } = useToast();
|
||||
const [inviteQuery, setInviteQuery] = useState('');
|
||||
const [inviteResults, setInviteResults] = useState<FamilyInviteCandidate[]>([]);
|
||||
const [inviteSearching, setInviteSearching] = useState(false);
|
||||
const [selectedInviteUser, setSelectedInviteUser] = useState<FamilyInviteCandidate | null>(null);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
setInviteQuery('');
|
||||
setInviteResults([]);
|
||||
setSelectedInviteUser(null);
|
||||
return;
|
||||
}
|
||||
if (!token || inviteQuery.trim().length < 2) {
|
||||
setInviteResults([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = window.setTimeout(() => {
|
||||
setInviteSearching(true);
|
||||
searchFamilyInviteUsers(groupId, inviteQuery.trim(), token)
|
||||
.then((response) => setInviteResults(response.users ?? []))
|
||||
.catch(() => setInviteResults([]))
|
||||
.finally(() => setInviteSearching(false));
|
||||
}, 300);
|
||||
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [groupId, inviteQuery, open, token]);
|
||||
|
||||
async function submitInvite() {
|
||||
if (!token || !selectedInviteUser) return;
|
||||
setSubmitting(true);
|
||||
try {
|
||||
await sendFamilyInvite(groupId, { inviteeUserId: selectedInviteUser.id }, token);
|
||||
showToast('Приглашение отправлено');
|
||||
onOpenChange(false);
|
||||
onInvited?.();
|
||||
} catch (error) {
|
||||
showToast(getApiErrorMessage(error, 'Не удалось отправить приглашение') ?? 'Ошибка');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="rounded-[28px] sm:max-w-[420px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Пригласить в семью</DialogTitle>
|
||||
</DialogHeader>
|
||||
{selectedInviteUser ? (
|
||||
<div className="rounded-2xl bg-[#f4f5f8] px-4 py-3">
|
||||
<p className="font-medium">{selectedInviteUser.displayName}</p>
|
||||
<p className="text-sm text-[#667085]">
|
||||
{selectedInviteUser.email ?? selectedInviteUser.phone ?? selectedInviteUser.username ?? 'Контакт не указан'}
|
||||
</p>
|
||||
<button type="button" className="mt-2 text-sm text-[#667085] underline" onClick={() => setSelectedInviteUser(null)}>
|
||||
Выбрать другого пользователя
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
placeholder="ФИО, почта, телефон или логин"
|
||||
value={inviteQuery}
|
||||
onChange={(event) => setInviteQuery(event.target.value)}
|
||||
/>
|
||||
<div className="mt-2 max-h-56 overflow-y-auto rounded-2xl border border-[#eceef4]">
|
||||
{inviteSearching ? (
|
||||
<div className="flex items-center gap-2 px-4 py-3 text-sm text-[#667085]">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Ищем пользователей...
|
||||
</div>
|
||||
) : inviteQuery.trim().length < 2 ? (
|
||||
<p className="px-4 py-3 text-sm text-[#667085]">Введите минимум 2 символа для поиска</p>
|
||||
) : inviteResults.length ? (
|
||||
inviteResults.map((candidate) => (
|
||||
<button
|
||||
key={candidate.id}
|
||||
type="button"
|
||||
className="flex w-full items-center gap-3 border-b border-[#eceef4] px-4 py-3 text-left last:border-b-0 hover:bg-[#fafbfd]"
|
||||
onClick={() => {
|
||||
setSelectedInviteUser(candidate);
|
||||
setInviteResults([]);
|
||||
}}
|
||||
>
|
||||
<AvatarWithPresence
|
||||
userId={candidate.id}
|
||||
displayName={candidate.displayName}
|
||||
hasAvatar={candidate.hasAvatar}
|
||||
token={token}
|
||||
isVerified={candidate.isVerified}
|
||||
verificationIcon={candidate.verificationIcon}
|
||||
className="h-9 w-9"
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="truncate font-medium">{candidate.displayName}</p>
|
||||
<p className="truncate text-sm text-[#667085]">
|
||||
{candidate.email ?? candidate.phone ?? candidate.username ?? 'Без контактов'}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<p className="px-4 py-3 text-sm text-[#667085]">Пользователи не найдены</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Button className="mt-4 w-full rounded-xl" disabled={!selectedInviteUser || submitting} onClick={() => void submitInvite()}>
|
||||
{submitting ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Отправить приглашение'}
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user