Files
IdP/apps/frontend/components/chat/chat-tools-dialog.tsx
2026-06-26 00:16:17 +03:00

221 lines
8.3 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.
'use client';
import { useMemo, useState } from 'react';
import { CalendarDays, ImageIcon, Loader2, Search } from 'lucide-react';
import { Calendar } from '@/components/ui/calendar';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import type { ChatMessage } from '@/lib/api';
import { getChatDateKey } from '@/lib/chat-date-utils';
import { getMessagePreviewText } from '@/lib/chat-message-utils';
import { cn } from '@/lib/utils';
export type ChatToolsTab = 'search' | 'calendar' | 'media';
interface ChatToolsDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
messages: ChatMessage[];
defaultTab?: ChatToolsTab;
getMessagePreview: (message: ChatMessage) => string;
getMediaUrl?: (message: ChatMessage) => string | null;
onJumpToMessage: (messageId: string) => void;
}
export function ChatToolsDialog({
open,
onOpenChange,
messages,
defaultTab = 'search',
getMessagePreview,
getMediaUrl,
onJumpToMessage
}: ChatToolsDialogProps) {
const [tab, setTab] = useState<ChatToolsTab>(defaultTab);
const [query, setQuery] = useState('');
const [selectedDate, setSelectedDate] = useState<Date | undefined>();
const messageDates = useMemo(() => {
const keys = new Set<string>();
for (const message of messages) {
keys.add(getChatDateKey(message.createdAt));
}
return keys;
}, [messages]);
const searchResults = useMemo(() => {
const normalized = query.trim().toLowerCase();
if (!normalized) return [];
return messages.filter((message) => getMessagePreview(message).toLowerCase().includes(normalized));
}, [getMessagePreview, messages, query]);
const mediaMessages = useMemo(
() => messages.filter((message) => message.type === 'IMAGE' && message.storageKey),
[messages]
);
const calendarModifiers = useMemo(
() => ({
hasMessages: (date: Date) => messageDates.has(getChatDateKey(date))
}),
[messageDates]
);
function jumpToDate(date: Date) {
const key = getChatDateKey(date);
const target = messages.find((message) => getChatDateKey(message.createdAt) === key);
if (!target) return;
onJumpToMessage(target.id);
onOpenChange(false);
}
function handleOpenChange(next: boolean) {
onOpenChange(next);
if (next) setTab(defaultTab);
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-h-[85vh] overflow-hidden rounded-[28px] sm:max-w-[520px]">
<DialogHeader>
<DialogTitle>Чат</DialogTitle>
</DialogHeader>
<div className="flex gap-1 rounded-xl bg-[#f4f5f8] p-1">
{(
[
{ id: 'search' as const, label: 'Поиск', icon: Search },
{ id: 'calendar' as const, label: 'Дата', icon: CalendarDays },
{ id: 'media' as const, label: 'Медиа', icon: ImageIcon }
] as const
).map((item) => (
<button
key={item.id}
type="button"
onClick={() => setTab(item.id)}
className={cn(
'flex flex-1 items-center justify-center gap-1.5 rounded-lg px-2 py-2 text-xs font-medium transition',
tab === item.id ? 'bg-white text-[#1f2430] shadow-sm' : 'text-[#667085] hover:text-[#1f2430]'
)}
>
<item.icon className="h-3.5 w-3.5" />
{item.label}
</button>
))}
</div>
<div className="max-h-[min(52vh,420px)] overflow-y-auto pt-1">
{tab === 'search' ? (
<div className="space-y-3">
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#a8adbc]" />
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Поиск по сообщениям"
className="rounded-xl pl-9"
/>
</div>
{!query.trim() ? (
<p className="py-6 text-center text-sm text-[#667085]">Введите текст для поиска</p>
) : searchResults.length ? (
<div className="space-y-1">
{searchResults.map((message) => (
<button
key={message.id}
type="button"
className="flex w-full flex-col rounded-xl px-3 py-2 text-left transition hover:bg-[#f4f5f8]"
onClick={() => {
onJumpToMessage(message.id);
onOpenChange(false);
}}
>
<span className="text-xs font-medium text-[#3390ec]">{message.senderName}</span>
<span className="truncate text-sm text-[#1f2430]">{getMessagePreview(message)}</span>
<span className="text-[11px] text-[#a8adbc]">
{new Intl.DateTimeFormat('ru-RU', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
}).format(new Date(message.createdAt))}
</span>
</button>
))}
</div>
) : (
<p className="py-6 text-center text-sm text-[#667085]">Ничего не найдено</p>
)}
</div>
) : null}
{tab === 'calendar' ? (
<div className="space-y-3">
<Calendar
mode="single"
selected={selectedDate}
onSelect={(date) => {
setSelectedDate(date);
if (date) jumpToDate(date);
}}
modifiers={calendarModifiers}
modifiersClassNames={{
hasMessages: 'relative after:absolute after:bottom-1 after:left-1/2 after:h-1 after:w-1 after:-translate-x-1/2 after:rounded-full after:bg-[#3390ec]'
}}
/>
<div className="space-y-1">
<label className="text-xs font-medium text-[#667085]">Перейти к дате</label>
<Input
type="date"
onChange={(event) => {
if (!event.target.value) return;
jumpToDate(new Date(`${event.target.value}T12:00:00`));
}}
/>
</div>
<p className="text-xs text-[#667085]">Точки под датами означают наличие сообщений в этот день</p>
</div>
) : null}
{tab === 'media' ? (
mediaMessages.length ? (
<div className="grid grid-cols-3 gap-2">
{mediaMessages.map((message) => {
const url = getMediaUrl?.(message);
return (
<button
key={message.id}
type="button"
className="relative aspect-square overflow-hidden rounded-xl bg-[#eef1f6]"
onClick={() => {
onJumpToMessage(message.id);
onOpenChange(false);
}}
>
{url ? (
// eslint-disable-next-line @next/next/no-img-element
<img src={url} alt="" className="h-full w-full object-cover" />
) : (
<div className="flex h-full items-center justify-center">
<Loader2 className="h-5 w-5 animate-spin text-[#667085]" />
</div>
)}
</button>
);
})}
</div>
) : (
<p className="py-8 text-center text-sm text-[#667085]">Медиафайлов пока нет</p>
)
) : null}
</div>
</DialogContent>
</Dialog>
);
}
export function buildMessagePreview(message: ChatMessage, visibleText?: string) {
return getMessagePreviewText(message, visibleText);
}