|
<!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> |