veza/apps/web/src/features/chat/components/MessageSearch.tsx

134 lines
4.3 KiB
TypeScript

import { useState } from 'react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Search, X } from 'lucide-react';
import { apiClient } from '@/services/api/client';
import { logger } from '@/utils/logger';
import { ChatMessage } from '../store/chatStore';
import { parseApiError } from '@/utils/apiErrorHandler';
// FE-PAGE-005: Complete Chat page implementation - Message Search
interface MessageSearchProps {
conversationId: string;
onMessageSelect?: (messageId: string) => void;
}
export function MessageSearch({
conversationId,
onMessageSelect,
}: MessageSearchProps) {
const [searchQuery, setSearchQuery] = useState('');
const [searchResults, setSearchResults] = useState<ChatMessage[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [showResults, setShowResults] = useState(false);
const handleSearch = async () => {
if (!searchQuery.trim() || !conversationId) return;
try {
setIsSearching(true);
setShowResults(true);
// Assuming backend has a search endpoint - if not, we'll search client-side
const response = await apiClient.get(
`/conversations/${conversationId}/messages/search`,
{
params: { q: searchQuery, limit: 20 },
},
);
setSearchResults(response.data.messages || []);
} catch (error: unknown) {
const apiError = parseApiError(error);
// If endpoint doesn't exist, fall back to client-side search
logger.warn('Search endpoint not available or failed', {
error: apiError.message,
});
// We'll implement client-side search as fallback
setSearchResults([]);
} finally {
setIsSearching(false);
}
};
const handleClear = () => {
setSearchQuery('');
setSearchResults([]);
setShowResults(false);
};
return (
<div className="relative">
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSearch();
}
}}
placeholder="Search messages..."
className="pl-8 pr-8"
/>
{searchQuery && (
<Button
variant="ghost"
size="sm"
className="absolute right-1 top-1/2 transform -translate-y-1/2 h-6 w-6 p-0"
onClick={handleClear}
>
<X className="h-4 w-4" />
</Button>
)}
</div>
<Button
onClick={handleSearch}
disabled={!searchQuery.trim() || isSearching}
size="sm"
>
{isSearching ? 'Searching...' : 'Search'}
</Button>
</div>
{showResults && searchResults.length > 0 && (
<div className="absolute z-10 w-full mt-2 bg-white border rounded-lg shadow-lg max-h-64 overflow-y-auto">
<div className="p-2">
<div className="text-xs text-gray-500 mb-2">
{searchResults.length} result(s) found
</div>
{searchResults.map((message) => (
<div
key={message.id}
className="p-2 hover:bg-gray-100 rounded cursor-pointer"
onClick={() => {
onMessageSelect?.(message.id);
setShowResults(false);
}}
>
<div className="text-sm font-medium">
{message.sender_username}
</div>
<div className="text-xs text-gray-600 truncate">
{message.content}
</div>
<div className="text-xs text-gray-400">
{new Date(message.created_at).toLocaleString()}
</div>
</div>
))}
</div>
</div>
)}
{showResults && searchResults.length === 0 && searchQuery && (
<div className="absolute z-10 w-full mt-2 bg-white border rounded-lg shadow-lg p-4 text-sm text-gray-500">
No messages found
</div>
)}
</div>
);
}