| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Excel Row Comparison Tool</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| .file-input-label { |
| transition: all 0.3s ease; |
| } |
| .file-input-label:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); |
| } |
| .file-input-label.drag-over { |
| border-color: #4f46e5; |
| background-color: #eef2ff; |
| } |
| .result-table { |
| max-height: 400px; |
| overflow-y: auto; |
| } |
| .loading-spinner { |
| animation: spin 1s linear infinite; |
| } |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| <div class="max-w-4xl mx-auto"> |
| |
| <header class="text-center mb-10"> |
| <h1 class="text-3xl md:text-4xl font-bold text-indigo-700 mb-2">Excel Row Comparison Tool</h1> |
| <p class="text-gray-600">Upload two Excel files to find matching rows between them</p> |
| </header> |
|
|
| |
| <main class="bg-white rounded-xl shadow-md overflow-hidden"> |
| |
| <section class="p-6 border-b border-gray-200"> |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
| |
| <div> |
| <label for="file1" class="block text-sm font-medium text-gray-700 mb-2"> |
| <i class="fas fa-file-excel text-green-600 mr-1"></i> |
| First Excel File |
| </label> |
| <div class="relative"> |
| <label for="file1" class="file-input-label flex flex-col items-center justify-center w-full h-40 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100"> |
| <div class="flex flex-col items-center justify-center pt-5 pb-6"> |
| <i class="fas fa-cloud-upload-alt text-3xl text-gray-400 mb-3"></i> |
| <p class="mb-2 text-sm text-gray-500"> |
| <span class="font-semibold">Click to upload</span> or drag and drop |
| </p> |
| <p class="text-xs text-gray-500">XLSX or XLS files only</p> |
| </div> |
| <input id="file1" type="file" class="hidden" accept=".xlsx,.xls" /> |
| </label> |
| <div id="file1-name" class="mt-2 text-sm text-gray-600 truncate max-w-full"></div> |
| </div> |
| </div> |
|
|
| |
| <div> |
| <label for="file2" class="block text-sm font-medium text-gray-700 mb-2"> |
| <i class="fas fa-file-excel text-blue-600 mr-1"></i> |
| Second Excel File |
| </label> |
| <div class="relative"> |
| <label for="file2" class="file-input-label flex flex-col items-center justify-center w-full h-40 border-2 border-gray-300 border-dashed rounded-lg cursor-pointer bg-gray-50 hover:bg-gray-100"> |
| <div class="flex flex-col items-center justify-center pt-5 pb-6"> |
| <i class="fas fa-cloud-upload-alt text-3xl text-gray-400 mb-3"></i> |
| <p class="mb-2 text-sm text-gray-500"> |
| <span class="font-semibold">Click to upload</span> or drag and drop |
| </p> |
| <p class="text-xs text-gray-500">XLSX or XLS files only</p> |
| </div> |
| <input id="file2" type="file" class="hidden" accept=".xlsx,.xls" /> |
| </label> |
| <div id="file2-name" class="mt-2 text-sm text-gray-600 truncate max-w-full"></div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="mt-6"> |
| <h3 class="text-sm font-medium text-gray-700 mb-2">Comparison Options</h3> |
| <div class="flex flex-wrap gap-4"> |
| <label class="inline-flex items-center"> |
| <input type="checkbox" id="ignore-case" class="rounded text-indigo-600 focus:ring-indigo-500"> |
| <span class="ml-2 text-sm text-gray-600">Ignore case differences</span> |
| </label> |
| <label class="inline-flex items-center"> |
| <input type="checkbox" id="trim-whitespace" checked class="rounded text-indigo-600 focus:ring-indigo-500"> |
| <span class="ml-2 text-sm text-gray-600">Trim whitespace</span> |
| </label> |
| </div> |
| </div> |
|
|
| |
| <div class="mt-8 text-center"> |
| <button id="compare-btn" class="px-6 py-3 bg-indigo-600 text-white font-medium rounded-lg hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" disabled> |
| <i class="fas fa-exchange-alt mr-2"></i> Compare Files |
| </button> |
| </div> |
| </section> |
|
|
| |
| <section id="results-section" class="hidden p-6"> |
| <div class="flex justify-between items-center mb-4"> |
| <h2 class="text-xl font-semibold text-gray-800"> |
| <i class="fas fa-clipboard-check text-indigo-600 mr-2"></i> |
| Matching Rows Found |
| </h2> |
| <div class="flex items-center space-x-2"> |
| <span id="match-count" class="px-3 py-1 bg-indigo-100 text-indigo-800 text-sm font-medium rounded-full">0 matches</span> |
| <button id="export-btn" class="px-4 py-2 bg-green-600 text-white text-sm font-medium rounded-lg hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 transition-colors"> |
| <i class="fas fa-file-export mr-1"></i> Export |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="loading-indicator" class="hidden flex justify-center items-center py-10"> |
| <div class="loading-spinner h-10 w-10 border-4 border-indigo-500 border-t-transparent rounded-full"></div> |
| </div> |
|
|
| |
| <div id="results-container" class="hidden"> |
| <div class="result-table border border-gray-200 rounded-lg overflow-hidden"> |
| <table class="min-w-full divide-y divide-gray-200"> |
| <thead class="bg-gray-50"> |
| <tr id="table-headers"></tr> |
| </thead> |
| <tbody id="table-body" class="bg-white divide-y divide-gray-200"></tbody> |
| </table> |
| </div> |
| <p id="no-results" class="hidden text-center py-6 text-gray-500"> |
| No matching rows found between the two files. |
| </p> |
| </div> |
|
|
| |
| <div id="stats" class="hidden mt-4 grid grid-cols-1 md:grid-cols-3 gap-4 text-sm text-gray-600"> |
| <div class="bg-gray-50 p-3 rounded-lg"> |
| <div class="font-medium">File 1 Rows</div> |
| <div id="file1-rows" class="text-indigo-600 font-semibold">0</div> |
| </div> |
| <div class="bg-gray-50 p-3 rounded-lg"> |
| <div class="font-medium">File 2 Rows</div> |
| <div id="file2-rows" class="text-indigo-600 font-semibold">0</div> |
| </div> |
| <div class="bg-gray-50 p-3 rounded-lg"> |
| <div class="font-medium">Processing Time</div> |
| <div id="processing-time" class="text-indigo-600 font-semibold">0ms</div> |
| </div> |
| </div> |
| </section> |
| </main> |
|
|
| |
| <footer class="mt-10 text-center text-sm text-gray-500"> |
| <p>This tool compares rows between two Excel files in your browser. Your data never leaves your computer.</p> |
| <p class="mt-1">For large files, consider using our <a href="#" class="text-indigo-600 hover:underline">desktop version</a>.</p> |
| </footer> |
| </div> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| const file1Input = document.getElementById('file1'); |
| const file2Input = document.getElementById('file2'); |
| const file1Name = document.getElementById('file1-name'); |
| const file2Name = document.getElementById('file2-name'); |
| const compareBtn = document.getElementById('compare-btn'); |
| const resultsSection = document.getElementById('results-section'); |
| const loadingIndicator = document.getElementById('loading-indicator'); |
| const resultsContainer = document.getElementById('results-container'); |
| const noResults = document.getElementById('no-results'); |
| const tableHeaders = document.getElementById('table-headers'); |
| const tableBody = document.getElementById('table-body'); |
| const matchCount = document.getElementById('match-count'); |
| const exportBtn = document.getElementById('export-btn'); |
| const file1Rows = document.getElementById('file1-rows'); |
| const file2Rows = document.getElementById('file2-rows'); |
| const processingTime = document.getElementById('processing-time'); |
| const stats = document.getElementById('stats'); |
| const ignoreCase = document.getElementById('ignore-case'); |
| const trimWhitespace = document.getElementById('trim-whitespace'); |
| |
| |
| let file1Data = null; |
| let file2Data = null; |
| let headers = []; |
| let commonRows = []; |
| |
| |
| setupDragDrop(file1Input, document.querySelector('label[for="file1"]')); |
| setupDragDrop(file2Input, document.querySelector('label[for="file2"]')); |
| |
| |
| file1Input.addEventListener('change', function(e) { |
| handleFileSelect(e, file1Name, 1); |
| }); |
| |
| file2Input.addEventListener('change', function(e) { |
| handleFileSelect(e, file2Name, 2); |
| }); |
| |
| |
| compareBtn.addEventListener('click', compareFiles); |
| |
| |
| exportBtn.addEventListener('click', exportResults); |
| |
| |
| function setupDragDrop(inputElement, labelElement) { |
| labelElement.addEventListener('dragover', function(e) { |
| e.preventDefault(); |
| this.classList.add('drag-over'); |
| }); |
| |
| labelElement.addEventListener('dragleave', function(e) { |
| e.preventDefault(); |
| this.classList.remove('drag-over'); |
| }); |
| |
| labelElement.addEventListener('drop', function(e) { |
| e.preventDefault(); |
| this.classList.remove('drag-over'); |
| if (e.dataTransfer.files.length) { |
| inputElement.files = e.dataTransfer.files; |
| const event = new Event('change'); |
| inputElement.dispatchEvent(event); |
| } |
| }); |
| } |
| |
| function handleFileSelect(event, nameElement, fileNumber) { |
| const file = event.target.files[0]; |
| if (!file) return; |
| |
| |
| nameElement.textContent = file.name; |
| nameElement.title = file.name; |
| |
| |
| const reader = new FileReader(); |
| reader.onload = function(e) { |
| try { |
| const data = new Uint8Array(e.target.result); |
| const workbook = XLSX.read(data, { type: 'array' }); |
| const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; |
| const jsonData = XLSX.utils.sheet_to_json(firstSheet, { header: 1 }); |
| |
| |
| if (fileNumber === 1) { |
| file1Data = jsonData; |
| } else { |
| file2Data = jsonData; |
| } |
| |
| |
| if (file1Data && file2Data) { |
| compareBtn.disabled = false; |
| } |
| } catch (error) { |
| showError(`Error reading file ${fileNumber}: ${error.message}`); |
| if (fileNumber === 1) { |
| file1Name.textContent = ''; |
| file1Data = null; |
| } else { |
| file2Name.textContent = ''; |
| file2Data = null; |
| } |
| compareBtn.disabled = true; |
| } |
| }; |
| reader.onerror = function() { |
| showError(`Error reading file ${fileNumber}`); |
| if (fileNumber === 1) { |
| file1Name.textContent = ''; |
| file1Data = null; |
| } else { |
| file2Name.textContent = ''; |
| file2Data = null; |
| } |
| compareBtn.disabled = true; |
| }; |
| reader.readAsArrayBuffer(file); |
| } |
| |
| function compareFiles() { |
| |
| resultsSection.classList.remove('hidden'); |
| loadingIndicator.classList.remove('hidden'); |
| resultsContainer.classList.add('hidden'); |
| noResults.classList.add('hidden'); |
| stats.classList.add('hidden'); |
| compareBtn.disabled = true; |
| compareBtn.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Processing...'; |
| |
| |
| setTimeout(() => { |
| const startTime = performance.now(); |
| |
| try { |
| |
| headers = file1Data[0] || []; |
| |
| |
| const rows1 = file1Data.slice(1); |
| const rows2 = file2Data.slice(1); |
| |
| |
| file1Rows.textContent = rows1.length; |
| file2Rows.textContent = rows2.length; |
| |
| |
| const processedRows1 = preprocessRows(rows1); |
| const processedRows2 = preprocessRows(rows2); |
| |
| |
| commonRows = findCommonRows(processedRows1, processedRows2); |
| |
| |
| updateResultsUI(commonRows); |
| |
| |
| const endTime = performance.now(); |
| processingTime.textContent = `${Math.round(endTime - startTime)}ms`; |
| stats.classList.remove('hidden'); |
| |
| } catch (error) { |
| showError(`Error during comparison: ${error.message}`); |
| } finally { |
| |
| loadingIndicator.classList.add('hidden'); |
| compareBtn.disabled = false; |
| compareBtn.innerHTML = '<i class="fas fa-exchange-alt mr-2"></i> Compare Files'; |
| } |
| }, 100); |
| } |
| |
| function preprocessRows(rows) { |
| return rows.map(row => { |
| return row.map(cell => { |
| let value = cell === undefined || cell === null ? '' : String(cell); |
| |
| if (trimWhitespace.checked) { |
| value = value.trim(); |
| } |
| |
| if (ignoreCase.checked) { |
| value = value.toLowerCase(); |
| } |
| |
| return value; |
| }); |
| }); |
| } |
| |
| function findCommonRows(rows1, rows2) { |
| |
| const rowStrings1 = rows1.map(row => JSON.stringify(row)); |
| const rowStrings2 = rows2.map(row => JSON.stringify(row)); |
| |
| |
| const set1 = new Set(rowStrings1); |
| const set2 = new Set(rowStrings2); |
| |
| |
| const commonStrings = [...set1].filter(rowStr => set2.has(rowStr)); |
| |
| |
| return rows1.filter(row => commonStrings.includes(JSON.stringify(row))); |
| } |
| |
| function updateResultsUI(commonRows) { |
| if (commonRows.length === 0) { |
| noResults.classList.remove('hidden'); |
| resultsContainer.classList.add('hidden'); |
| matchCount.textContent = '0 matches'; |
| return; |
| } |
| |
| |
| matchCount.textContent = `${commonRows.length} ${commonRows.length === 1 ? 'match' : 'matches'}`; |
| |
| |
| tableHeaders.innerHTML = ''; |
| tableBody.innerHTML = ''; |
| |
| |
| headers.forEach(header => { |
| const th = document.createElement('th'); |
| th.className = 'px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider'; |
| th.textContent = header || 'Column'; |
| tableHeaders.appendChild(th); |
| }); |
| |
| |
| commonRows.forEach((row, rowIndex) => { |
| const tr = document.createElement('tr'); |
| tr.className = rowIndex % 2 === 0 ? 'bg-white' : 'bg-gray-50'; |
| |
| row.forEach((cell, cellIndex) => { |
| const td = document.createElement('td'); |
| td.className = 'px-6 py-4 whitespace-nowrap text-sm text-gray-900'; |
| td.textContent = cell; |
| |
| |
| if (rowIndex === 0 && cellIndex < headers.length && cell === headers[cellIndex]) { |
| td.className += ' font-semibold text-indigo-600'; |
| } |
| |
| tr.appendChild(td); |
| }); |
| |
| tableBody.appendChild(tr); |
| }); |
| |
| resultsContainer.classList.remove('hidden'); |
| noResults.classList.add('hidden'); |
| } |
| |
| function exportResults() { |
| if (!commonRows.length) { |
| showError('No results to export'); |
| return; |
| } |
| |
| try { |
| |
| const wb = XLSX.utils.book_new(); |
| |
| |
| const exportData = [headers, ...commonRows]; |
| |
| |
| const ws = XLSX.utils.aoa_to_sheet(exportData); |
| |
| |
| XLSX.utils.book_append_sheet(wb, ws, "Matching Rows"); |
| |
| |
| XLSX.writeFile(wb, 'matching_rows.xlsx'); |
| |
| } catch (error) { |
| showError(`Error exporting results: ${error.message}`); |
| } |
| } |
| |
| function showError(message) { |
| |
| alert(`Error: ${message}`); |
| } |
| }); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=chagtptmm/excel-row-comparison-tool" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |