| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Photo Uploader | Share Your Moments</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| .upload-area { |
| transition: all 0.3s ease; |
| } |
| .upload-area.drag-over { |
| border-color: #4f46e5; |
| background-color: #f5f3ff; |
| } |
| .photo-card { |
| transition: transform 0.3s ease, box-shadow 0.3s ease; |
| } |
| .photo-card:hover { |
| transform: translateY(-5px); |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); |
| } |
| .progress-bar { |
| transition: width 0.3s ease; |
| } |
| @keyframes fadeIn { |
| from { opacity: 0; transform: scale(0.95); } |
| to { opacity: 1; transform: scale(1); } |
| } |
| .fade-in { |
| animation: fadeIn 0.3s ease-out forwards; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 min-h-screen"> |
| <div class="container mx-auto px-4 py-12"> |
| |
| <header class="text-center mb-12"> |
| <h1 class="text-4xl font-bold text-indigo-600 mb-2">Photo Vault</h1> |
| <p class="text-gray-600 max-w-lg mx-auto">Upload and store your favorite moments. Your photos are saved until you delete them.</p> |
| </header> |
|
|
| |
| <main> |
| |
| <div class="max-w-3xl mx-auto bg-white rounded-xl shadow-md overflow-hidden mb-12"> |
| <div class="p-8"> |
| <div |
| id="uploadArea" |
| class="upload-area border-2 border-dashed border-gray-300 rounded-lg p-12 text-center cursor-pointer hover:border-indigo-400" |
| > |
| <div class="flex flex-col items-center justify-center"> |
| <i class="fas fa-cloud-upload-alt text-5xl text-indigo-500 mb-4"></i> |
| <h3 class="text-xl font-semibold text-gray-700 mb-2">Drag & Drop your photos here</h3> |
| <p class="text-gray-500 mb-6">or click to browse files</p> |
| <input |
| type="file" |
| id="fileInput" |
| class="hidden" |
| accept="image/*" |
| multiple |
| > |
| <button |
| id="selectFilesBtn" |
| class="bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-6 rounded-lg transition duration-200" |
| > |
| Select Files |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="progressContainer" class="mt-6 hidden"> |
| <div class="flex justify-between mb-1"> |
| <span class="text-sm font-medium text-gray-700">Uploading...</span> |
| <span id="progressPercent" class="text-sm font-medium text-gray-700">0%</span> |
| </div> |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> |
| <div id="progressBar" class="progress-bar bg-indigo-600 h-2.5 rounded-full" style="width: 0%"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="gallerySection" class="max-w-7xl mx-auto"> |
| <div class="flex justify-between items-center mb-6"> |
| <h2 class="text-2xl font-semibold text-gray-800">Your Photo Collection</h2> |
| <div id="photoCount" class="text-sm text-gray-500">0 photos</div> |
| </div> |
| |
| |
| <div id="emptyState" class="text-center py-12 bg-white rounded-lg shadow-sm"> |
| <i class="fas fa-images text-4xl text-gray-300 mb-4"></i> |
| <h3 class="text-xl font-medium text-gray-500">Your photo vault is empty</h3> |
| <p class="text-gray-400 mt-2">Upload your first photo to start your collection</p> |
| </div> |
| |
| |
| <div id="photoGrid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 hidden"> |
| |
| </div> |
| </div> |
| </main> |
|
|
| |
| <footer class="mt-16 text-center text-gray-500 text-sm"> |
| <p>© 2023 Photo Vault. Your photos are stored locally in your browser.</p> |
| </footer> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| const uploadArea = document.getElementById('uploadArea'); |
| const fileInput = document.getElementById('fileInput'); |
| const selectFilesBtn = document.getElementById('selectFilesBtn'); |
| const progressContainer = document.getElementById('progressContainer'); |
| const progressBar = document.getElementById('progressBar'); |
| const progressPercent = document.getElementById('progressPercent'); |
| const photoGrid = document.getElementById('photoGrid'); |
| const emptyState = document.getElementById('emptyState'); |
| const gallerySection = document.getElementById('gallerySection'); |
| const photoCount = document.getElementById('photoCount'); |
| |
| |
| loadSavedPhotos(); |
| |
| |
| selectFilesBtn.addEventListener('click', () => fileInput.click()); |
| |
| |
| fileInput.addEventListener('change', handleFiles); |
| |
| |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
| uploadArea.addEventListener(eventName, preventDefaults, false); |
| }); |
| |
| function preventDefaults(e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| } |
| |
| ['dragenter', 'dragover'].forEach(eventName => { |
| uploadArea.addEventListener(eventName, highlight, false); |
| }); |
| |
| ['dragleave', 'drop'].forEach(eventName => { |
| uploadArea.addEventListener(eventName, unhighlight, false); |
| }); |
| |
| function highlight() { |
| uploadArea.classList.add('drag-over'); |
| } |
| |
| function unhighlight() { |
| uploadArea.classList.remove('drag-over'); |
| } |
| |
| |
| uploadArea.addEventListener('drop', function(e) { |
| const dt = e.dataTransfer; |
| const files = dt.files; |
| handleFiles({ target: { files } }); |
| }); |
| |
| |
| function handleFiles(e) { |
| const files = e.target.files; |
| if (!files.length) return; |
| |
| |
| progressContainer.classList.remove('hidden'); |
| |
| |
| let progress = 0; |
| const interval = setInterval(() => { |
| progress += Math.random() * 10; |
| if (progress >= 100) { |
| progress = 100; |
| clearInterval(interval); |
| |
| |
| setTimeout(() => { |
| progressContainer.classList.add('hidden'); |
| }, 1000); |
| } |
| |
| progressBar.style.width = `${progress}%`; |
| progressPercent.textContent = `${Math.round(progress)}%`; |
| }, 200); |
| |
| |
| Array.from(files).forEach(file => { |
| if (!file.type.match('image.*')) return; |
| |
| const reader = new FileReader(); |
| |
| reader.onload = function(e) { |
| |
| const photo = { |
| id: Date.now().toString(36) + Math.random().toString(36).substr(2), |
| name: file.name, |
| size: file.size, |
| dataUrl: e.target.result, |
| uploadedAt: new Date().toISOString() |
| }; |
| |
| |
| savePhoto(photo); |
| |
| |
| displayPhoto(photo); |
| |
| |
| updatePhotoCount(); |
| }; |
| |
| reader.readAsDataURL(file); |
| }); |
| } |
| |
| |
| function loadSavedPhotos() { |
| const savedPhotos = getSavedPhotos(); |
| if (savedPhotos.length > 0) { |
| emptyState.classList.add('hidden'); |
| photoGrid.classList.remove('hidden'); |
| |
| savedPhotos.forEach(photo => { |
| displayPhoto(photo); |
| }); |
| |
| updatePhotoCount(); |
| } |
| } |
| |
| |
| function getSavedPhotos() { |
| const savedPhotos = localStorage.getItem('photoVault'); |
| return savedPhotos ? JSON.parse(savedPhotos) : []; |
| } |
| |
| |
| function savePhoto(photo) { |
| const savedPhotos = getSavedPhotos(); |
| savedPhotos.unshift(photo); |
| localStorage.setItem('photoVault', JSON.stringify(savedPhotos)); |
| } |
| |
| |
| function removePhoto(id) { |
| const savedPhotos = getSavedPhotos(); |
| const updatedPhotos = savedPhotos.filter(photo => photo.id !== id); |
| localStorage.setItem('photoVault', JSON.stringify(updatedPhotos)); |
| updatePhotoCount(); |
| } |
| |
| |
| function displayPhoto(photo) { |
| |
| if (photoGrid.children.length === 0) { |
| emptyState.classList.add('hidden'); |
| photoGrid.classList.remove('hidden'); |
| } |
| |
| |
| const photoCard = document.createElement('div'); |
| photoCard.className = 'photo-card bg-white rounded-lg overflow-hidden shadow-md hover:shadow-lg fade-in'; |
| photoCard.dataset.id = photo.id; |
| |
| photoCard.innerHTML = ` |
| <div class="relative pb-[100%]"> |
| <img src="${photo.dataUrl}" alt="${photo.name}" class="absolute h-full w-full object-cover"> |
| </div> |
| <div class="p-4"> |
| <div class="flex justify-between items-center"> |
| <h3 class="font-medium text-gray-800 truncate">${photo.name}</h3> |
| <span class="text-xs text-gray-500">${formatFileSize(photo.size)}</span> |
| </div> |
| <div class="flex justify-between mt-3"> |
| <button class="text-indigo-600 hover:text-indigo-800 text-sm font-medium download-btn"> |
| <i class="fas fa-download mr-1"></i> Download |
| </button> |
| <button class="text-red-500 hover:text-red-700 text-sm font-medium delete-btn"> |
| <i class="fas fa-trash mr-1"></i> Delete |
| </button> |
| </div> |
| </div> |
| `; |
| |
| |
| photoCard.querySelector('.delete-btn').addEventListener('click', function() { |
| photoCard.classList.add('opacity-0', 'scale-95'); |
| setTimeout(() => { |
| |
| photoGrid.removeChild(photoCard); |
| |
| |
| removePhoto(photo.id); |
| |
| |
| if (photoGrid.children.length === 0) { |
| emptyState.classList.remove('hidden'); |
| photoGrid.classList.add('hidden'); |
| } |
| }, 300); |
| }); |
| |
| |
| photoCard.querySelector('.download-btn').addEventListener('click', function() { |
| downloadPhoto(photo.dataUrl, photo.name); |
| }); |
| |
| |
| photoGrid.prepend(photoCard); |
| } |
| |
| |
| function downloadPhoto(dataUrl, filename) { |
| const link = document.createElement('a'); |
| link.href = dataUrl; |
| link.download = filename; |
| document.body.appendChild(link); |
| link.click(); |
| document.body.removeChild(link); |
| } |
| |
| |
| function updatePhotoCount() { |
| const count = getSavedPhotos().length; |
| photoCount.textContent = `${count} ${count === 1 ? 'photo' : 'photos'}`; |
| } |
| |
| |
| function formatFileSize(bytes) { |
| if (bytes === 0) return '0 Bytes'; |
| const k = 1024; |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
| } |
| }); |
| </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=privateuserh/sampgallery-vbeta1-00" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |