| <!DOCTYPE html> |
| <html lang="en" class="dark"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>ImageMatch | Visual Search</title> |
| <link rel="icon" type="image/x-icon" href="/static/favicon.ico"> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script> |
| tailwind.config = { |
| darkMode: 'class', |
| theme: { |
| extend: { |
| colors: { |
| primary: { |
| 500: '#3b82f6', |
| }, |
| secondary: { |
| 500: '#6366f1', |
| } |
| } |
| } |
| } |
| } |
| </script> |
| <script src="https://unpkg.com/feather-icons"></script> |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
| <style> |
| .dropzone { |
| transition: all 0.3s ease; |
| } |
| .dropzone-active { |
| border-color: #3b82f6; |
| background-color: rgba(59, 130, 246, 0.05); |
| } |
| .result-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); |
| gap: 1.5rem; |
| } |
| @media (max-width: 640px) { |
| .result-grid { |
| grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); |
| } |
| } |
| .image-card { |
| transition: transform 0.2s ease, box-shadow 0.2s ease; |
| } |
| .image-card:hover { |
| transform: translateY(-4px); |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2); |
| } |
| </style> |
| </head> |
| <body class="bg-gray-900 text-gray-100 min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| <header class="mb-12 text-center"> |
| <h1 class="text-4xl font-bold mb-2 bg-gradient-to-r from-primary-500 to-secondary-500 bg-clip-text text-transparent">ImageMatch</h1> |
| <p class="text-gray-400 max-w-2xl mx-auto">Upload an image to find visually similar matches from our collection</p> |
| </header> |
|
|
| <main class="max-w-4xl mx-auto"> |
| <div class="mb-10"> |
| <div |
| id="dropzone" |
| class="dropzone border-2 border-dashed border-gray-700 rounded-xl p-8 text-center cursor-pointer hover:border-gray-600 transition-colors" |
| > |
| <div class="flex flex-col items-center justify-center"> |
| <i data-feather="upload" class="w-12 h-12 text-primary-500 mb-4"></i> |
| <h2 class="text-xl font-semibold mb-2">Drag & drop your image here</h2> |
| <p class="text-gray-400 mb-4">or click to browse files</p> |
| <input type="file" id="fileInput" class="hidden" accept="image/*"> |
| <button id="browseBtn" class="px-4 py-2 bg-primary-500 hover:bg-primary-600 rounded-md transition-colors"> |
| Select Image |
| </button> |
| </div> |
| </div> |
| <div id="previewContainer" class="mt-4 flex justify-center hidden"> |
| <div class="relative inline-block"> |
| <img id="previewImage" src="" alt="Preview" class="max-h-48 rounded-md shadow-lg"> |
| <button id="removeImageBtn" class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 transition-colors"> |
| <i data-feather="x" class="w-4 h-4"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="bg-gray-800 rounded-xl p-6 mb-10"> |
| <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6"> |
| <h3 class="text-xl font-semibold">Search Settings</h3> |
| <div class="flex items-center space-x-4"> |
| <div> |
| <label for="resultCount" class="block text-sm font-medium text-gray-300 mb-1">Number of Results</label> |
| <div class="flex items-center space-x-2"> |
| <input |
| type="range" |
| id="resultCount" |
| min="3" |
| max="10" |
| value="5" |
| class="w-full h-2 bg-gray-700 rounded-lg appearance-none cursor-pointer" |
| > |
| <span id="countValue" class="w-8 text-center">5</span> |
| </div> |
| </div> |
| <button |
| id="searchBtn" |
| class="px-4 py-2 bg-secondary-500 hover:bg-secondary-600 rounded-md transition-colors flex items-center space-x-2" |
| disabled |
| > |
| <i data-feather="search" class="w-4 h-4"></i> |
| <span>Find Matches</span> |
| </button> |
| </div> |
| </div> |
|
|
| <div class="flex items-center space-x-2 mb-4"> |
| <i data-feather="info" class="w-4 h-4 text-blue-400"></i> |
| <p class="text-sm text-gray-400">For best results, use clear, high-quality images without excessive text.</p> |
| </div> |
| </div> |
|
|
| <div id="resultsSection" class="hidden"> |
| <div class="flex justify-between items-center mb-6"> |
| <h3 class="text-2xl font-semibold">Similar Images</h3> |
| <div class="flex items-center space-x-2"> |
| <span class="text-sm text-gray-400">Sort by:</span> |
| <select class="bg-gray-800 border border-gray-700 text-gray-300 rounded-md px-3 py-1 text-sm"> |
| <option>Most Similar</option> |
| <option>Color</option> |
| <option>Size</option> |
| </select> |
| </div> |
| </div> |
|
|
| <div class="bg-gray-800 rounded-xl p-6"> |
| <div id="loadingIndicator" class="hidden flex justify-center items-center py-12"> |
| <div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-500"></div> |
| </div> |
|
|
| <div id="resultsContainer" class="result-grid"> |
| |
| </div> |
|
|
| <div id="noResults" class="hidden text-center py-12"> |
| <i data-feather="frown" class="w-12 h-12 mx-auto text-gray-500 mb-4"></i> |
| <h4 class="text-xl font-medium text-gray-400 mb-2">No matches found</h4> |
| <p class="text-gray-500">Try uploading a different image or adjusting your search parameters.</p> |
| </div> |
| </div> |
| </div> |
| </main> |
|
|
| <footer class="mt-20 text-center text-gray-500 text-sm"> |
| <p>© 2023 ImageMatch. All rights reserved.</p> |
| </footer> |
| </div> |
|
|
| <script> |
| feather.replace(); |
| |
| |
| const dropzone = document.getElementById('dropzone'); |
| const fileInput = document.getElementById('fileInput'); |
| const browseBtn = document.getElementById('browseBtn'); |
| const previewContainer = document.getElementById('previewContainer'); |
| const previewImage = document.getElementById('previewImage'); |
| const removeImageBtn = document.getElementById('removeImageBtn'); |
| const searchBtn = document.getElementById('searchBtn'); |
| const resultCount = document.getElementById('resultCount'); |
| const countValue = document.getElementById('countValue'); |
| const resultsSection = document.getElementById('resultsSection'); |
| const loadingIndicator = document.getElementById('loadingIndicator'); |
| const resultsContainer = document.getElementById('resultsContainer'); |
| const noResults = document.getElementById('noResults'); |
| |
| |
| resultCount.addEventListener('input', () => { |
| countValue.textContent = resultCount.value; |
| }); |
| |
| |
| browseBtn.addEventListener('click', () => { |
| fileInput.click(); |
| }); |
| |
| |
| fileInput.addEventListener('change', (e) => { |
| handleFileSelection(e.target.files[0]); |
| }); |
| |
| |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
| dropzone.addEventListener(eventName, preventDefaults, false); |
| }); |
| |
| function preventDefaults(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| |
| ['dragenter', 'dragover'].forEach(eventName => { |
| dropzone.addEventListener(eventName, highlight, false); |
| }); |
| |
| ['dragleave', 'drop'].forEach(eventName => { |
| dropzone.addEventListener(eventName, unhighlight, false); |
| }); |
| |
| function highlight() { |
| dropzone.classList.add('dropzone-active'); |
| } |
| |
| function unhighlight() { |
| dropzone.classList.remove('dropzone-active'); |
| } |
| |
| dropzone.addEventListener('drop', (e) => { |
| const dt = e.dataTransfer; |
| const file = dt.files[0]; |
| handleFileSelection(file); |
| }); |
| |
| |
| function handleFileSelection(file) { |
| if (!file.type.match('image.*')) { |
| alert('Please select an image file'); |
| return; |
| } |
| |
| const reader = new FileReader(); |
| |
| reader.onload = (e) => { |
| previewImage.src = e.target.result; |
| previewContainer.classList.remove('hidden'); |
| searchBtn.disabled = false; |
| }; |
| |
| reader.readAsDataURL(file); |
| } |
| |
| |
| removeImageBtn.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| previewImage.src = ''; |
| previewContainer.classList.add('hidden'); |
| fileInput.value = ''; |
| searchBtn.disabled = true; |
| resultsSection.classList.add('hidden'); |
| }); |
| |
| |
| searchBtn.addEventListener('click', () => { |
| |
| resultsSection.classList.remove('hidden'); |
| loadingIndicator.classList.remove('hidden'); |
| resultsContainer.innerHTML = ''; |
| noResults.classList.add('hidden'); |
| |
| |
| setTimeout(() => { |
| loadingIndicator.classList.add('hidden'); |
| |
| |
| const mockResults = Array(parseInt(resultCount.value)).fill().map((_, i) => ({ |
| id: i, |
| similarity: (90 - (i * 5)) + '%', |
| url: `http://static.photos/technology/320x240/${100 + i}` |
| })); |
| |
| if (mockResults.length > 0) { |
| resultsContainer.innerHTML = mockResults.map(result => ` |
| <div class="image-card bg-gray-700 rounded-lg overflow-hidden group"> |
| <div class="relative pt-[100%]"> |
| <img src="${result.url}" alt="Result ${result.id}" class="absolute top-0 left-0 w-full h-full object-cover"> |
| <div class="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/70 to-transparent p-3 opacity-0 group-hover:opacity-100 transition-opacity"> |
| <div class="flex justify-between items-end"> |
| <span class="text-sm font-medium">Similarity: ${result.similarity}</span> |
| <button class="text-white hover:text-primary-500 transition-colors"> |
| <i data-feather="download" class="w-4 h-4"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| `).join(''); |
| |
| feather.replace(); |
| } else { |
| noResults.classList.remove('hidden'); |
| } |
| }, 1500); |
| }); |
| </script> |
| </body> |
| </html> |
|
|