Spaces:
Running
Running
| // Global state management | |
| const state = { | |
| searchResults: [], | |
| recentSearches: [], | |
| user: null, | |
| isLoading: false, | |
| notifications: [] | |
| }; | |
| // Initialize the application | |
| document.addEventListener('DOMContentLoaded', function() { | |
| initializeApp(); | |
| setupEventListeners(); | |
| loadRecentSearches(); | |
| checkUserSession(); | |
| }); | |
| // Initialize application | |
| function initializeApp() { | |
| // Initialize tooltips | |
| initTooltips(); | |
| // Load saved preferences | |
| loadPreferences(); | |
| // Start activity monitoring | |
| startActivityMonitoring(); | |
| // Initialize web components | |
| initializeComponents(); | |
| } | |
| // Setup event listeners | |
| function setupEventListeners() { | |
| // Search form submission | |
| const searchForm = document.getElementById('searchForm'); | |
| if (searchForm) { | |
| searchForm.addEventListener('submit', handleSearchSubmit); | |
| } | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', handleKeyboardShortcuts); | |
| // Window resize handler | |
| window.addEventListener('resize', handleWindowResize); | |
| // Online/offline detection | |
| window.addEventListener('online', handleOnlineStatus); | |
| window.addEventListener('offline', handleOfflineStatus); | |
| } | |
| // Handle search form submission | |
| async function handleSearchSubmit(e) { | |
| e.preventDefault(); | |
| const formData = new FormData(e.target); | |
| const searchParams = { | |
| firstName: formData.get('firstName'), | |
| lastName: formData.get('lastName'), | |
| dob: formData.get('dob'), | |
| phone: formData.get('phone'), | |
| email: formData.get('email'), | |
| address: formData.get('address') | |
| }; | |
| // Validate required fields | |
| if (!searchParams.firstName && !searchParams.lastName && !searchParams.phone && !searchParams.email) { | |
| showNotification('Please enter at least one search parameter', 'warning'); | |
| return; | |
| } | |
| // Show loading state | |
| setLoadingState(true); | |
| try { | |
| // Perform search | |
| const results = await performSearch(searchParams); | |
| // Store results | |
| state.searchResults = results; | |
| // Add to recent searches | |
| addToRecentSearches(searchParams, results); | |
| // Show results modal | |
| displaySearchResults(results); | |
| // Show success notification | |
| showNotification(`Found ${results.length} result(s)`, 'success'); | |
| } catch (error) { | |
| console.error('Search error:', error); | |
| showNotification('Search failed. Please try again.', 'error'); | |
| } finally { | |
| setLoadingState(false); | |
| } | |
| } | |
| // Perform actual search | |
| async function performSearch(params) { | |
| // Simulate API call with mock data | |
| await new Promise(resolve => setTimeout(resolve, 1500)); | |
| // Generate mock results based on search parameters | |
| const mockResults = generateMockResults(params); | |
| // In a real implementation, this would be an actual API call: | |
| // const response = await fetch('/api/skip-trace/search', { | |
| // method: 'POST', | |
| // headers: { 'Content-Type': 'application/json' }, | |
| // body: JSON.stringify(params) | |
| // }); | |
| // return response.json(); | |
| return mockResults; | |
| } | |
| // Generate mock search results | |
| function generateMockResults(params) { | |
| const results = []; | |
| const numResults = Math.floor(Math.random() * 5) + 1; | |
| for (let i = 0; i < numResults; i++) { | |
| const confidence = Math.floor(Math.random() * 40) + 60; | |
| const hasAssets = Math.random() > 0.5; | |
| results.push({ | |
| id: `result-${Date.now()}-${i}`, | |
| fullName: `${params.firstName || 'John'} ${params.lastName || 'Doe'} ${i > 0 ? ` (${i + 1})` : ''}`, | |
| firstName: params.firstName || 'John', | |
| lastName: params.lastName || 'Doe', | |
| age: Math.floor(Math.random() * 50) + 25, | |
| confidence: confidence, | |
| confidenceLevel: confidence >= 85 ? 'high' : confidence >= 70 ? 'medium' : 'low', | |
| emails: [ | |
| `${(params.firstName || 'john').toLowerCase()}.${(params.lastName || 'doe').toLowerCase()}${i}@email.com`, | |
| `${(params.firstName || 'john').toLowerCase()}.${(params.lastName || 'doe').toLowerCase()}${i}@gmail.com` | |
| ], | |
| phones: [ | |
| `(${Math.floor(Math.random() * 900) + 100}) ${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 9000) + 1000}`, | |
| `+1 ${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 900) + 100}-${Math.floor(Math.random() * 9000) + 1000}` | |
| ], | |
| addresses: [ | |
| { | |
| street: `${Math.floor(Math.random() * 9999) + 1} Main St`, | |
| city: 'Los Angeles', | |
| state: 'CA', | |
| zip: `${Math.floor(Math.random() * 90000) + 10000}`, | |
| fullAddress: `${Math.floor(Math.random() * 9999) + 1} Main St, Los Angeles, CA ${Math.floor(Math.random() * 90000) + 10000}` | |
| } | |
| ], | |
| assets: hasAssets ? [ | |
| { | |
| type: 'Property', | |
| value: Math.floor(Math.random() * 1000000) + 200000, | |
| address: `${Math.floor(Math.random() * 9999) + 1} Oak Ave, Beverly Hills, CA`, | |
| description: 'Residential Property' | |
| }, | |
| { | |
| type: 'Vehicle', | |
| value: Math.floor(Math.random() * 50000) + 15000, | |
| description: `${['Honda', 'Toyota', 'Ford', 'BMW'][Math.floor(Math.random() * 4)]} ${2020 - Math.floor(Math.random() * 5)}` | |
| } | |
| ] : [], | |
| dataSources: ['Enformion', 'People Data Labs', 'Google'].slice(0, Math.floor(Math.random() * 3) + 1), | |
| lastUpdated: new Date().toISOString(), | |
| verified: confidence >= 85 | |
| }); | |
| } | |
| return results; | |
| } | |
| // Display search results in modal | |
| function displaySearchResults(results) { | |
| const searchModal = document.getElementById('searchModal'); | |
| const resultsContainer = document.getElementById('searchResults'); | |
| if (!searchModal || !resultsContainer) return; | |
| // Clear previous results | |
| resultsContainer.innerHTML = ''; | |
| if (results.length === 0) { | |
| resultsContainer.innerHTML = ` | |
| <div class="text-center py-8"> | |
| <i data-feather="search" class="w-16 h-16 text-gray-400 mx-auto mb-4"></i> | |
| <p class="text-gray-400">No results found</p> | |
| </div> | |
| `; | |
| } else { | |
| results.forEach((result, index) => { | |
| const resultElement = createResultElement(result, index); | |
| resultsContainer.appendChild(resultElement); | |
| }); | |
| } | |
| // Show modal | |
| searchModal.classList.remove('hidden'); | |
| // Re-initialize feather icons | |
| feather.replace(); | |
| } | |
| // Create result element | |
| function createResultElement(result, index) { | |
| const div = document.createElement('div'); | |
| div.className = 'search-result-item fade-in'; | |
| div.style.animationDelay = `${index * 0.1}s`; | |
| const confidenceClass = `confidence-${result.confidenceLevel}`; | |
| const confidenceColor = result.confidenceLevel === 'high' ? 'green' : | |
| result.confidenceLevel === 'medium' ? 'yellow' : 'red'; | |
| div.innerHTML = ` | |
| <div class="flex items-start justify-between mb-4"> | |
| <div class="flex items-center"> | |
| <div class="w-12 h-12 bg-gradient-to-br from-primary-500 to-secondary-500 rounded-full flex items-center justify-center mr-4"> | |
| <i data-feather="user" class="w-6 h-6 text-white"></i> | |
| </div> | |
| <div> | |
| <h3 class="text-lg font-semibold">${result.fullName}</h3> | |
| <p class="text-gray-400 text-sm">Age: ${result.age} • ID: ${result.id}</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <span class="confidence-score ${confidenceClass}"> | |
| ${result.confidence}% confidence | |
| </span> | |
| ${result.verified ? '<i data-feather="check-circle" class="w-5 h-5 text-green-500"></i>' : ''} | |
| </div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4"> | |
| <div> | |
| <h4 class="text-sm font-semibold text-gray-400 mb-2">Contact Information</h4> | |
| <div class="space-y-1"> | |
| ${result.emails.map(email => ` | |
| <div class="flex items-center"> | |
| <i data-feather="mail" class="w-4 h-4 text-gray-500 mr-2"></i> | |
| <span class="text-sm">${email}</span> | |
| </div> | |
| `).join('')} | |
| ${result.phones.map(phone => ` | |
| <div class="flex items-center"> | |
| <i data-feather="phone" class="w-4 h-4 text-gray-500 mr-2"></i> | |
| <span class="text-sm">${phone}</span> | |
| </div> | |
| `).join('')} | |
| </div> | |
| </div> | |
| <div> | |
| <h4 class="text-sm font-semibold text-gray-400 mb-2">Address</h4> | |
| <div class="space-y-1"> | |
| ${result.addresses.map(addr => ` | |
| <div class="flex items-start"> | |
| <i data-feather="map-pin" class="w-4 h-4 text-gray-500 mr-2 mt-0.5"></i> | |
| <span class="text-sm">${addr.fullAddress}</span> | |
| </div> | |
| `).join('')} | |
| </div> | |
| </div> | |
| </div> | |
| ${result.assets.length > 0 ? ` | |
| <div class="mb-4"> | |
| <h4 class="text-sm font-semibold text-gray-400 mb-2">Discovered Assets</h4> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-2"> | |
| ${result.assets.map(asset => ` | |
| <div class="asset-item"> | |
| <div class="flex items-center"> | |
| <i data-feather="${asset.type === 'Property' ? 'home' : 'truck'}" class="w-4 h-4 text-gray-400 mr-2"></i> | |
| <div> | |
| <p class="text-sm font-medium">${asset.type}: $${asset.value.toLocaleString()}</p> | |
| <p class="text-xs text-gray-500">${asset.description}</p> | |
| </div> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| </div> | |
| ` : ''} | |
| <div class="flex items-center justify-between border-t border-gray-700 pt-4"> | |
| <div class="flex flex-wrap gap-2"> | |
| ${result.dataSources.map(source => ` | |
| <span class="source-chip active">${source}</span> | |
| `).join('')} | |
| </div> | |
| <div class="flex space-x-2"> | |
| <button onclick="exportSingleResult('${result.id}')" class="px-3 py-1 bg-gray-700 text-gray-300 rounded hover:bg-gray-600 transition-colors text-sm"> | |
| <i data-feather="download" class="w-4 h-4 inline mr-1"></i> | |
| Export | |
| </button> | |
| <button onclick="viewFullReport('${result.id}')" class="px-3 py-1 bg-primary-500 text-white rounded hover:bg-primary-600 transition-colors text-sm"> | |
| <i data-feather="file-text" class="w-4 h-4 inline mr-1"></i> | |
| Full Report | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| return div; | |
| } | |
| // Modal functions | |
| function showSearchModal() { | |
| const modal = document.getElementById('searchModal'); | |
| if (modal) modal.classList.remove('hidden'); | |
| } | |
| function closeSearchModal() { | |
| const modal = document.getElementById('searchModal'); | |
| if (modal) modal.classList.add('hidden'); | |
| } | |
| function showBatchUpload() { | |
| const modal = document.getElementById('batchModal'); | |
| if (modal) modal.classList.remove('hidden'); | |
| } | |
| function closeBatchModal() { | |
| const modal = document.getElementById('batchModal'); | |
| if (modal) modal.classList.add('hidden'); | |
| } | |
| function showAnalytics() { | |
| const modal = document.getElementById('analyticsModal'); | |
| if (modal) modal.classList.remove('hidden'); | |
| } | |
| function closeAnalyticsModal() { | |
| const modal = document.getElementById('analyticsModal'); | |
| if (modal) modal.classList.add('hidden'); | |
| } | |
| function showAdvancedFilters() { | |
| showNotification('Advanced filters coming soon!', 'info'); | |
| } | |
| // Utility functions | |
| function clearForm() { | |
| const form = document.getElementById('searchForm'); | |
| if (form) form.reset(); | |
| } | |
| function downloadTemplate() { | |
| const csvContent = 'first_name,last_name,dob,phone,email,address\nJohn,Doe,1980-01-01,555-1234,john@example.com,123 Main St, City, State ZIP'; | |
| const blob = new Blob([csvContent], { type: 'text/csv' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'skip-trace-template.csv'; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| showNotification('Template downloaded successfully', 'success'); | |
| } | |
| function exportResults() { | |
| if (state.searchResults.length === 0) { | |
| showNotification('No results to export', 'warning'); | |
| return; | |
| } | |
| // Create CSV content | |
| const headers = ['Name', 'Confidence', 'Emails', 'Phones', 'Addresses', 'Assets']; | |
| const rows = state.searchResults.map(result => [ | |
| result.fullName, | |
| `${result.confidence}%`, | |
| result.emails.join('; '), | |
| result.phones.join('; '), | |
| result.addresses.map(a => a.fullAddress).join('; '), | |
| result.assets.map(a => `${a.type}: $${a.value}`).join('; ') | |
| ]); | |
| const csvContent = [headers, ...rows].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n'); | |
| // Download CSV | |
| const blob = new Blob([csvContent], { type: 'text/csv' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = `skip-trace-results-${new Date().toISOString().split('T')[0]}.csv`; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| showNotification('Results exported successfully', 'success'); | |
| } | |
| function exportSingleResult(resultId) { | |
| const result = state.searchResults.find(r => r.id === resultId); | |
| if (!result) return; | |
| // Similar to exportResults but for single result | |
| showNotification(`Exporting result for ${result.fullName}`, 'info'); | |
| } | |
| function viewFullReport(resultId) { | |
| const result = state.searchResults.find(r => r.id === resultId); | |
| if (!result) return; | |
| showNotification(`Generating full report for ${result.fullName}`, 'info'); | |
| // In a real implementation, this would generate a detailed PDF report | |
| } | |
| // Notification system | |
| function showNotification(message, type = 'info') { | |
| const notification = document.createElement('div'); | |
| notification.className = 'notification'; | |
| const colors = { | |
| success: 'text-green-400 border-green-500', | |
| error: 'text-red-400 border-red-500', | |
| warning: 'text-yellow-400 border-yellow-500', | |
| info: 'text-blue-400 border-blue-500' | |
| }; | |
| const icons = { | |
| success: 'check-circle', | |
| error: 'x-circle', | |
| warning: 'alert-triangle', | |
| info: 'info' | |
| }; | |
| notification.innerHTML = ` | |
| <div class="flex items-center"> | |
| <i data-feather="${icons[type]}" class="w-5 h-5 mr-3 ${colors[type].split(' ')[0]}"></i> | |
| <span class="${colors[type].split(' ')[0]}">${message}</span> | |
| </div> | |
| `; | |
| document.body.appendChild(notification); | |
| // Re-initialize feather icons | |
| feather.replace(); | |
| // Auto remove after 5 seconds | |
| setTimeout(() => { | |
| notification.style.animation = 'slideOutRight 0.3s ease-out'; | |
| setTimeout(() => { | |
| if (notification.parentNode) { | |
| notification.parentNode.removeChild(notification); | |
| } | |
| }, 300); | |
| }, 5000); | |
| } | |
| // Loading state management | |
| function setLoadingState(loading) { | |
| state.isLoading = loading; | |
| const submitButton = document.querySelector('button[type="submit"]'); | |
| if (submitButton) { | |
| if (loading) { | |
| submitButton.disabled = true; | |
| submitButton.innerHTML = '<div class="spinner inline-block mr-2"></div>Searching...'; | |
| } else { | |
| submitButton.disabled = false; | |
| submitButton.innerHTML = '<i data-feather="search" class="inline w-4 h-4 mr-2"></i>Search'; | |
| feather.replace(); | |
| } | |
| } | |
| } | |
| // Recent searches management | |
| function addToRecentSearches(params, results) { | |
| const search = { | |
| params, | |
| resultCount: results.length, | |
| timestamp: new Date().toISOString(), | |
| id: Date.now().toString() | |
| }; | |
| state.recentSearches.unshift(search); | |
| state.recentSearches = state.recentSearches.slice(0, 10); // Keep only last 10 | |
| saveRecentSearches(); | |
| updateRecentSearchesUI(); | |
| } | |
| function loadRecentSearches() { | |
| const saved = localStorage.getItem('recentSearches'); | |
| if (saved) { | |
| state.recentSearches = JSON.parse(saved); | |
| updateRecentSearchesUI(); | |
| } | |
| } | |
| function saveRecentSearches() { | |
| localStorage.setItem('recentSearches', JSON.stringify(state.recentSearches)); | |
| } | |
| function updateRecentSearchesUI() { | |
| const container = document.getElementById('recentSearches'); | |
| if (!container) return; | |
| if (state.recentSearches.length === 0) { | |
| container.innerHTML = '<p class="text-gray-500 text-center py-4">No recent searches</p>'; | |
| return; | |
| } | |
| container.innerHTML = state.recentSearches.slice(0, 3).map(search => { | |
| const timeAgo = getTimeAgo(new Date(search.timestamp)); | |
| const name = search.params.firstName || search.params.lastName || | |
| search.params.email || search.params.phone || 'Unknown'; | |
| return ` | |
| <div class="flex items-center justify-between p-3 bg-gray-700 rounded-lg hover:bg-gray-600 transition-colors cursor-pointer" onclick="repeatSearch(${search.id})"> | |
| <div class="flex items-center"> | |
| <div class="w-10 h-10 bg-primary-500/20 rounded-full flex items-center justify-center mr-3"> | |
| <i data-feather="user" class="w-5 h-5 text-primary-400"></i> | |
| </div> | |
| <div> | |
| <p class="font-semibold">${name}</p> | |
| <p class="text-xs text-gray-400">${timeAgo} • ${search.resultCount} results</p> | |
| </div> | |
| </div> | |
| <i data-feather="chevron-right" class="w-5 h-5 text-gray-400"></i> | |
| </div> | |
| `; | |
| }).join(''); | |
| feather.replace(); | |
| } | |
| function repeatSearch(searchId) { | |
| const search = state.recentSearches.find(s => s.id === searchId); | |
| if (!search) return; | |
| // Populate form with previous search parameters | |
| Object.entries(search.params).forEach(([key, value]) => { | |
| const input = document.getElementById(key); | |
| if (input) input.value = value || ''; | |
| }); | |
| // Trigger search | |
| document.getElementById('searchForm').dispatchEvent(new Event('submit')); | |
| } | |
| // Utility functions | |
| function getTimeAgo(date) { | |
| const seconds = Math.floor((new Date() - date) / 1000); | |
| let interval = seconds / 31536000; | |
| if (interval > 1) return Math.floor(interval) + ' years ago'; | |
| interval = seconds / 2592000; | |
| if (interval > 1) return Math.floor(interval) + ' months ago'; | |
| interval = seconds / 86400; | |
| if (interval > 1) return Math.floor(interval) + ' days ago'; | |
| interval = seconds / 3600; | |
| if (interval > 1) return Math.floor(interval) + ' hours ago'; | |
| interval = seconds / 60; | |
| if (interval > 1) return Math.floor(interval) + ' minutes ago'; | |
| return 'Just now'; | |
| } | |
| // Keyboard shortcuts | |
| function handleKeyboardShortcuts(e) { | |
| // Ctrl/Cmd + K for search | |
| if ((e.ctrlKey || e.metaKey) && e.key === 'k') { | |
| e.preventDefault(); | |
| document.getElementById('firstName')?.focus(); | |
| } | |
| // Escape to close modals | |
| if (e.key === 'Escape') { | |
| closeSearchModal(); | |
| closeBatchModal(); | |
| closeAnalyticsModal(); | |
| } | |
| } | |
| // Window resize handler | |
| function handleWindowResize() { | |
| // Handle responsive layout adjustments | |
| if (window.innerWidth < 768) { | |
| // Mobile adjustments | |
| } | |
| } | |
| // Online/offline handlers | |
| function handleOnlineStatus() { | |
| showNotification('Connection restored', 'success'); | |
| } | |
| function handleOfflineStatus() { | |
| showNotification('Connection lost. Some features may be unavailable.', 'warning'); | |
| } | |
| // User session management | |
| function checkUserSession() { | |
| const token = localStorage.getItem('authToken'); | |
| if (!token) { | |
| // Redirect to login or show guest mode | |
| console.log('No user session found'); | |
| } | |
| } | |
| // Activity monitoring | |
| function startActivityMonitoring() { | |
| let lastActivity = Date.now(); | |
| ['mousedown', 'keydown', 'scroll', 'touchstart'].forEach(event => { | |
| document.addEventListener(event, () => { | |
| lastActivity = Date.now(); | |
| }); | |
| }); | |
| // Check for inactivity every minute | |
| setInterval(() => { | |
| if (Date.now() - lastActivity > 30 * 60 * 1000) { // 30 minutes | |
| showNotification('Session about to expire due to inactivity', 'warning'); | |
| } | |
| }, 60000); | |
| } | |
| // Preferences management | |
| function loadPreferences() { | |
| const preferences = localStorage.getItem('userPreferences'); | |
| if (preferences) { | |
| const prefs = JSON.parse(preferences); | |
| // Apply preferences | |
| if (prefs.theme) { | |
| document.documentElement.className = prefs.theme; | |
| } | |
| } | |
| } | |
| // Tooltip initialization | |
| function initTooltips() { | |
| // Initialize tooltips if using a tooltip library | |
| console.log('Tooltips initialized'); | |
| } | |
| // Web component initialization | |
| function initializeComponents() { | |
| // Initialize any web components | |
| console.log('Web components initialized'); | |
| } | |
| // Export functions for global access | |
| window.showSearchModal = showSearchModal; | |
| window.closeSearchModal = closeSearchModal; | |
| window.showBatchUpload = showBatchUpload; | |
| window.closeBatchModal = closeBatchModal; | |
| window.showAnalytics = showAnalytics; | |
| window.closeAnalyticsModal = closeAnalyticsModal; | |
| window.showAdvancedFilters = showAdvancedFilters; | |
| window.clearForm = clearForm; | |
| window.downloadTemplate = downloadTemplate; | |
| window.exportResults = exportResults; | |
| window.exportSingleResult = exportSingleResult; | |
| window.viewFullReport = viewFullReport; | |
| window.repeatSearch = repeatSearch; |