![]() Server : Apache/2 System : Linux server-15-235-50-60 5.15.0-164-generic #174-Ubuntu SMP Fri Nov 14 20:25:16 UTC 2025 x86_64 User : gositeme ( 1004) PHP Version : 8.2.29 Disable Function : exec,system,passthru,shell_exec,proc_close,proc_open,dl,popen,show_source,posix_kill,posix_mkfifo,posix_getpwuid,posix_setpgid,posix_setsid,posix_setuid,posix_setgid,posix_seteuid,posix_setegid,posix_uname Directory : /home/gositeme/domains/lavocat.ca/private_html/src/components/ |
import React, { useState, useEffect, useCallback } from 'react';
import { useRouter } from 'next/router';
import { Search, Filter, X, Users, FileText, Building2, Gavel, Star, MapPin, Calendar, TrendingUp } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { useDebounce } from '@/hooks/useDebounce';
interface SearchResult {
id: string;
type: 'user' | 'case' | 'document' | 'business';
displayName: string;
displayDescription: string;
url: string;
relevance: number;
createdAt: string;
isVerified?: boolean;
averageRating?: number;
totalCases?: number;
wonCases?: number;
location?: string;
legalArea?: string;
jurisdiction?: string;
urgencyLevel?: string;
_count?: {
followers?: number;
supporters?: number;
reviews?: number;
offers?: number;
comments?: number;
};
}
interface SearchFacets {
types: {
users: number;
cases: number;
documents: number;
businesses: number;
};
categories: Record<string, number>;
jurisdictions: Record<string, number>;
verified: {
verified: number;
unverified: number;
};
}
interface AdvancedSearchProps {
initialQuery?: string;
className?: string;
onResultClick?: (result: SearchResult) => void;
}
const AdvancedSearch: React.FC<AdvancedSearchProps> = ({
initialQuery = '',
className = '',
onResultClick
}) => {
const router = useRouter();
const [query, setQuery] = useState(initialQuery);
const [results, setResults] = useState<SearchResult[]>([]);
const [facets, setFacets] = useState<SearchFacets | null>(null);
const [loading, setLoading] = useState(false);
const [showFilters, setShowFilters] = useState(false);
const [filters, setFilters] = useState({
type: 'all',
category: 'all',
jurisdiction: 'all',
verified: 'all',
sortBy: 'relevance'
});
const [suggestions, setSuggestions] = useState<string[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const [selectedSuggestion, setSelectedSuggestion] = useState(-1);
const [searchStart, setSearchStart] = useState<number | null>(null);
const debouncedQuery = useDebounce(query, 300);
// Search suggestions
const searchSuggestions = [
'criminal law', 'family law', 'immigration', 'human rights',
'corporate law', 'civil litigation', 'real estate', 'tax law',
'Montreal', 'Quebec', 'Toronto', 'Vancouver',
'urgent cases', 'pro bono', 'verified lawyers', 'top rated'
];
useEffect(() => {
if (debouncedQuery.length >= 2) {
performSearch();
generateSuggestions();
} else {
setResults([]);
setFacets(null);
setSuggestions([]);
}
}, [debouncedQuery, filters]);
const performSearch = async () => {
if (!debouncedQuery.trim()) return;
setLoading(true);
setSearchStart(Date.now());
try {
const params = new URLSearchParams({
q: debouncedQuery,
...filters,
page: '1',
limit: '50'
});
const response = await fetch(`/api/search/global?${params}`);
if (response.ok) {
const data = await response.json();
setResults(data.results || []);
setFacets(data.facets || null);
// Analytics: track search event
if (typeof window !== 'undefined') {
fetch('/api/search/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: debouncedQuery,
filters,
resultCount: (data.results || []).length,
searchTime: searchStart ? Date.now() - searchStart : 0
})
}).catch(() => {});
}
}
} catch (error) {
console.error('Search error:', error);
} finally {
setLoading(false);
setSearchStart(null);
}
};
const generateSuggestions = () => {
const filtered = searchSuggestions.filter(suggestion =>
suggestion.toLowerCase().includes(debouncedQuery.toLowerCase())
);
setSuggestions(filtered.slice(0, 5));
};
const handleResultClick = (result: SearchResult) => {
// Analytics: track result click
if (typeof window !== 'undefined') {
fetch('/api/search/analytics', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query,
filters,
resultCount: results.length,
clickedResult: result.id,
searchTime: 0
})
}).catch(() => {});
}
if (onResultClick) {
onResultClick(result);
} else {
router.push(result.url);
}
};
const handleSuggestionClick = (suggestion: string) => {
setQuery(suggestion);
setShowSuggestions(false);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedSuggestion(prev =>
prev < suggestions.length - 1 ? prev + 1 : prev
);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedSuggestion(prev => prev > 0 ? prev - 1 : -1);
} else if (e.key === 'Enter' && selectedSuggestion >= 0) {
e.preventDefault();
handleSuggestionClick(suggestions[selectedSuggestion]);
} else if (e.key === 'Escape') {
setShowSuggestions(false);
setSelectedSuggestion(-1);
}
};
const getTypeIcon = (type: string) => {
switch (type) {
case 'user': return <Users className="h-4 w-4" />;
case 'case': return <Gavel className="h-4 w-4" />;
case 'document': return <FileText className="h-4 w-4" />;
case 'business': return <Building2 className="h-4 w-4" />;
default: return <Search className="h-4 w-4" />;
}
};
const getTypeColor = (type: string) => {
switch (type) {
case 'user': return 'bg-blue-100 text-blue-800';
case 'case': return 'bg-green-100 text-green-800';
case 'document': return 'bg-purple-100 text-purple-800';
case 'business': return 'bg-orange-100 text-orange-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const getTypeLabel = (type: string) => {
switch (type) {
case 'user': return 'Professional';
case 'case': return 'Case';
case 'document': return 'Document';
case 'business': return 'Business';
default: return type;
}
};
return (
<div className={`relative ${className}`}>
{/* Search Input */}
<div className="relative">
<Search className="absolute left-4 top-1/2 transform -translate-y-1/2 h-5 w-5 text-gray-400" />
<input
type="text"
placeholder="Search professionals, cases, documents, businesses..."
value={query}
onChange={(e) => {
setQuery(e.target.value);
setShowSuggestions(true);
setSelectedSuggestion(-1);
}}
onKeyDown={handleKeyDown}
onFocus={() => setShowSuggestions(true)}
className="w-full pl-12 pr-20 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<div className="absolute right-2 top-1/2 transform -translate-y-1/2 flex items-center space-x-2">
{loading && (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600"></div>
)}
<button
onClick={() => setShowFilters(!showFilters)}
className={`p-1 rounded ${showFilters ? 'bg-blue-100 text-blue-600' : 'text-gray-400 hover:text-gray-600'}`}
>
<Filter className="h-4 w-4" />
</button>
{query && (
<button
onClick={() => setQuery('')}
className="p-1 text-gray-400 hover:text-gray-600"
>
<X className="h-4 w-4" />
</button>
)}
</div>
</div>
{/* Search Suggestions */}
<AnimatePresence>
{showSuggestions && suggestions.length > 0 && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
className="absolute top-full left-0 right-0 mt-1 bg-white border border-gray-200 rounded-lg shadow-lg z-50"
>
{suggestions.map((suggestion, index) => (
<button
key={suggestion}
onClick={() => handleSuggestionClick(suggestion)}
className={`w-full px-4 py-2 text-left hover:bg-gray-50 flex items-center space-x-2 ${
index === selectedSuggestion ? 'bg-blue-50' : ''
}`}
>
<Search className="h-4 w-4 text-gray-400" />
<span>{suggestion}</span>
</button>
))}
</motion.div>
)}
</AnimatePresence>
{/* Advanced Filters */}
<AnimatePresence>
{showFilters && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
className="mt-4 bg-white border border-gray-200 rounded-lg p-4"
>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
{/* Type Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Type
</label>
<select
value={filters.type}
onChange={(e) => setFilters(prev => ({ ...prev, type: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Types</option>
<option value="users">Professionals</option>
<option value="cases">Cases</option>
<option value="documents">Documents</option>
<option value="businesses">Businesses</option>
</select>
</div>
{/* Category Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Category
</label>
<select
value={filters.category}
onChange={(e) => setFilters(prev => ({ ...prev, category: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Categories</option>
<option value="criminal">Criminal Law</option>
<option value="civil">Civil Law</option>
<option value="family">Family Law</option>
<option value="immigration">Immigration</option>
<option value="corporate">Corporate Law</option>
<option value="human_rights">Human Rights</option>
</select>
</div>
{/* Jurisdiction Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Jurisdiction
</label>
<select
value={filters.jurisdiction}
onChange={(e) => setFilters(prev => ({ ...prev, jurisdiction: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
>
<option value="all">All Jurisdictions</option>
<option value="quebec">Quebec</option>
<option value="ontario">Ontario</option>
<option value="british_columbia">British Columbia</option>
<option value="alberta">Alberta</option>
<option value="federal">Federal</option>
</select>
</div>
{/* Verification Filter */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Verification
</label>
<select
value={filters.verified}
onChange={(e) => setFilters(prev => ({ ...prev, verified: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
>
<option value="all">All</option>
<option value="verified">Verified Only</option>
<option value="unverified">Unverified</option>
</select>
</div>
{/* Sort By */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Sort By
</label>
<select
value={filters.sortBy}
onChange={(e) => setFilters(prev => ({ ...prev, sortBy: e.target.value }))}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500"
>
<option value="relevance">Relevance</option>
<option value="newest">Newest</option>
<option value="popular">Most Popular</option>
</select>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
{/* Search Results */}
{results.length > 0 && (
<div className="mt-6">
{/* Results Summary */}
<div className="flex items-center justify-between mb-4">
<div className="text-sm text-gray-600">
{results.length} results for "{query}"
</div>
{facets && (
<div className="flex items-center space-x-4 text-sm text-gray-500">
{Object.entries(facets.types).map(([type, count]) => (
count > 0 && (
<span key={type} className="flex items-center space-x-1">
{getTypeIcon(type)}
<span>{count} {getTypeLabel(type)}s</span>
</span>
)
))}
</div>
)}
</div>
{/* Results List */}
<div className="space-y-3">
{results.map((result) => (
<motion.div
key={`${result.type}-${result.id}`}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
className="bg-white border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow cursor-pointer"
onClick={() => handleResultClick(result)}
>
<div className="flex items-start space-x-3">
<div className={`p-2 rounded-lg ${getTypeColor(result.type)}`}>
{getTypeIcon(result.type)}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-2 mb-1">
<h3 className="text-lg font-semibold text-gray-900 truncate">
{result.displayName}
</h3>
{result.isVerified && (
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
Verified
</span>
)}
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getTypeColor(result.type)}`}>
{getTypeLabel(result.type)}
</span>
</div>
<p className="text-gray-600 text-sm mb-2 line-clamp-2">
{result.displayDescription}
</p>
<div className="flex items-center space-x-4 text-xs text-gray-500">
{result.location && (
<span className="flex items-center space-x-1">
<MapPin className="h-3 w-3" />
<span>{result.location}</span>
</span>
)}
{result.averageRating && (
<span className="flex items-center space-x-1">
<Star className="h-3 w-3" />
<span>{result.averageRating.toFixed(1)}</span>
</span>
)}
{result.totalCases && (
<span className="flex items-center space-x-1">
<Gavel className="h-3 w-3" />
<span>{result.totalCases} cases</span>
</span>
)}
{result._count?.followers && (
<span className="flex items-center space-x-1">
<Users className="h-3 w-3" />
<span>{result._count.followers} followers</span>
</span>
)}
{result._count?.supporters && (
<span className="flex items-center space-x-1">
<TrendingUp className="h-3 w-3" />
<span>{result._count.supporters} supporters</span>
</span>
)}
<span className="flex items-center space-x-1">
<Calendar className="h-3 w-3" />
<span>{new Date(result.createdAt).toLocaleDateString()}</span>
</span>
</div>
</div>
<div className="text-right">
<div className="text-xs text-gray-400">
{Math.round(result.relevance)}% match
</div>
</div>
</div>
</motion.div>
))}
</div>
</div>
)}
{/* No Results */}
{!loading && query.length >= 2 && results.length === 0 && (
<div className="mt-6 text-center py-8">
<Search className="h-12 w-12 text-gray-300 mx-auto mb-4" />
<h3 className="text-lg font-medium text-gray-900 mb-2">No results found</h3>
<p className="text-gray-600">
Try adjusting your search terms or filters to find what you're looking for.
</p>
</div>
)}
</div>
);
};
export default AdvancedSearch;