Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Easy Gallery System</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://unpkg.com/@supabase/supabase-js@2"></script> | |
<style> | |
.dropzone { | |
border: 2px dashed #ccc; | |
transition: all 0.3s; | |
} | |
.dropzone.active { | |
border-color: #4f46e5; | |
background-color: #f0f7ff; | |
} | |
.gallery-item { | |
transition: transform 0.3s, box-shadow 0.3s; | |
} | |
.gallery-item:hover { | |
transform: translateY(-5px); | |
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50 min-h-screen"> | |
<div class="container mx-auto px-4 py-8"> | |
<header class="mb-8 text-center"> | |
<h1 class="text-4xl font-bold text-indigo-700 mb-2">Easy Gallery System</h1> | |
<p class="text-gray-600">Upload and manage your media with ease</p> | |
</header> | |
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8"> | |
<!-- Upload Section --> | |
<div class="bg-white rounded-xl shadow-md p-6"> | |
<h2 class="text-2xl font-semibold text-gray-800 mb-4">Upload Media</h2> | |
<div id="dropzone" class="dropzone rounded-lg p-8 mb-4 text-center cursor-pointer"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /> | |
</svg> | |
<p class="mt-2 text-gray-600">Drag & drop files here or click to browse</p> | |
<input type="file" id="fileInput" class="hidden" multiple accept="image/*,video/*"> | |
</div> | |
<div class="mb-4"> | |
<label for="description" class="block text-sm font-medium text-gray-700 mb-1">Description</label> | |
<textarea id="description" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500" placeholder="Add a description for your media..."></textarea> | |
</div> | |
<button id="uploadBtn" class="w-full bg-indigo-600 hover:bg-indigo-700 text-white font-medium py-2 px-4 rounded-md transition duration-300 flex items-center justify-center"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"> | |
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" /> | |
</svg> | |
Upload to Gallery | |
</button> | |
<div id="uploadProgress" class="mt-4 hidden"> | |
<div class="flex justify-between mb-1"> | |
<span class="text-sm font-medium text-indigo-700">Uploading...</span> | |
<span id="progressPercent" class="text-sm font-medium text-indigo-700">0%</span> | |
</div> | |
<div class="w-full bg-gray-200 rounded-full h-2.5"> | |
<div id="progressBar" class="bg-indigo-600 h-2.5 rounded-full" style="width: 0%"></div> | |
</div> | |
</div> | |
</div> | |
<!-- Gallery Section --> | |
<div class="bg-white rounded-xl shadow-md p-6"> | |
<div class="flex justify-between items-center mb-4"> | |
<h2 class="text-2xl font-semibold text-gray-800">Your Gallery</h2> | |
<div class="relative"> | |
<select id="filterSelect" class="appearance-none bg-gray-100 border border-gray-300 text-gray-700 py-2 px-4 pr-8 rounded-md focus:outline-none focus:ring-2 focus:ring-indigo-500"> | |
<option value="all">All Media</option> | |
<option value="image">Images</option> | |
<option value="video">Videos</option> | |
</select> | |
<div class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 text-gray-700"> | |
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z"/></svg> | |
</div> | |
</div> | |
</div> | |
<div id="galleryContainer" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"> | |
<!-- Gallery items will be loaded here --> | |
<div class="text-center py-8 text-gray-500" id="emptyGalleryMessage"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /> | |
</svg> | |
Your gallery is empty. Upload some media to get started! | |
</div> | |
</div> | |
<div id="pagination" class="mt-6 flex justify-center hidden"> | |
<button id="prevPage" class="px-4 py-2 mx-1 rounded-md bg-gray-200 text-gray-700 disabled:opacity-50">← Previous</button> | |
<span id="pageInfo" class="px-4 py-2 mx-1">Page 1 of 1</span> | |
<button id="nextPage" class="px-4 py-2 mx-1 rounded-md bg-gray-200 text-gray-700 disabled:opacity-50">Next →</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Media Modal --> | |
<div id="mediaModal" class="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50 hidden"> | |
<div class="bg-white rounded-lg max-w-4xl w-full max-h-screen overflow-auto"> | |
<div class="flex justify-between items-center p-4 border-b"> | |
<h3 id="modalTitle" class="text-lg font-semibold"></h3> | |
<button id="closeModal" class="text-gray-500 hover:text-gray-700"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | |
</svg> | |
</button> | |
</div> | |
<div class="p-4"> | |
<div id="modalContent" class="flex justify-center"> | |
<!-- Media will be displayed here --> | |
</div> | |
<p id="modalDescription" class="mt-4 text-gray-700"></p> | |
<p id="modalDate" class="mt-2 text-sm text-gray-500"></p> | |
</div> | |
</div> | |
</div> | |
<script> | |
// Initialize Supabase | |
const supabaseUrl = 'https://kjioumobitojvvlsfewp.supabase.co'; | |
const supabaseKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImtqaW91bW9iaXRvanZ2bHNmZXdwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTUzNzg5NjksImV4cCI6MjA3MDk1NDk2OX0.oH2_2B7K4Uyw-glERw1bTgxXUdoyaZ1k7TM57E1KRkc'; | |
const supabase = supabase.createClient(supabaseUrl, supabaseKey); | |
// DOM Elements | |
const dropzone = document.getElementById('dropzone'); | |
const fileInput = document.getElementById('fileInput'); | |
const uploadBtn = document.getElementById('uploadBtn'); | |
const description = document.getElementById('description'); | |
const uploadProgress = document.getElementById('uploadProgress'); | |
const progressBar = document.getElementById('progressBar'); | |
const progressPercent = document.getElementById('progressPercent'); | |
const galleryContainer = document.getElementById('galleryContainer'); | |
const emptyGalleryMessage = document.getElementById('emptyGalleryMessage'); | |
const filterSelect = document.getElementById('filterSelect'); | |
const pagination = document.getElementById('pagination'); | |
const prevPage = document.getElementById('prevPage'); | |
const nextPage = document.getElementById('nextPage'); | |
const pageInfo = document.getElementById('pageInfo'); | |
const mediaModal = document.getElementById('mediaModal'); | |
const modalTitle = document.getElementById('modalTitle'); | |
const modalContent = document.getElementById('modalContent'); | |
const modalDescription = document.getElementById('modalDescription'); | |
const modalDate = document.getElementById('modalDate'); | |
const closeModal = document.getElementById('closeModal'); | |
// State | |
let currentFiles = []; | |
let currentPage = 1; | |
const itemsPerPage = 9; | |
let currentFilter = 'all'; | |
// Event Listeners | |
dropzone.addEventListener('click', () => fileInput.click()); | |
dropzone.addEventListener('dragover', (e) => { | |
e.preventDefault(); | |
dropzone.classList.add('active'); | |
}); | |
dropzone.addEventListener('dragleave', () => { | |
dropzone.classList.remove('active'); | |
}); | |
dropzone.addEventListener('drop', (e) => { | |
e.preventDefault(); | |
dropzone.classList.remove('active'); | |
if (e.dataTransfer.files.length) { | |
fileInput.files = e.dataTransfer.files; | |
handleFilesSelected(); | |
} | |
}); | |
fileInput.addEventListener('change', handleFilesSelected); | |
uploadBtn.addEventListener('click', uploadFiles); | |
filterSelect.addEventListener('change', (e) => { | |
currentFilter = e.target.value; | |
currentPage = 1; | |
loadGalleryItems(); | |
}); | |
prevPage.addEventListener('click', () => { | |
if (currentPage > 1) { | |
currentPage--; | |
loadGalleryItems(); | |
} | |
}); | |
nextPage.addEventListener('click', () => { | |
currentPage++; | |
loadGalleryItems(); | |
}); | |
closeModal.addEventListener('click', () => { | |
mediaModal.classList.add('hidden'); | |
}); | |
// Initialize | |
loadGalleryItems(); | |
// Functions | |
function handleFilesSelected() { | |
currentFiles = Array.from(fileInput.files); | |
if (currentFiles.length > 0) { | |
const fileNames = currentFiles.map(file => file.name).join(', '); | |
dropzone.innerHTML = ` | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /> | |
</svg> | |
<p class="mt-2 font-medium text-gray-700">${currentFiles.length} file(s) selected</p> | |
<p class="text-sm text-gray-500 truncate">${fileNames}</p> | |
`; | |
} | |
} | |
async function uploadFiles() { | |
if (currentFiles.length === 0) { | |
alert('Please select files to upload'); | |
return; | |
} | |
const desc = description.value.trim(); | |
uploadProgress.classList.remove('hidden'); | |
uploadBtn.disabled = true; | |
try { | |
for (let i = 0; i < currentFiles.length; i++) { | |
const file = currentFiles[i]; | |
const fileType = file.type.startsWith('image/') ? 'image' : 'video'; | |
const fileName = `${Date.now()}-${file.name}`; | |
const filePath = `${fileType}s/${fileName}`; | |
// Update progress | |
const progress = Math.round((i / currentFiles.length) * 100); | |
progressBar.style.width = `${progress}%`; | |
progressPercent.textContent = `${progress}%`; | |
// Upload file to storage | |
const { data: uploadData, error: uploadError } = await supabase.storage | |
.from('media') | |
.upload(filePath, file); | |
if (uploadError) throw uploadError; | |
// Get public URL | |
const { data: urlData } = supabase.storage | |
.from('media') | |
.getPublicUrl(filePath); | |
// Insert record into database | |
const { error: dbError } = await supabase | |
.from('gallery') | |
.insert([ | |
{ | |
url: urlData.publicUrl, | |
type: fileType, | |
description: desc, | |
filename: fileName, | |
created_at: new Date().toISOString() | |
} | |
]); | |
if (dbError) throw dbError; | |
} | |
// Complete progress | |
progressBar.style.width = '100%'; | |
progressPercent.textContent = '100%'; | |
// Reset form | |
setTimeout(() => { | |
uploadProgress.classList.add('hidden'); | |
uploadBtn.disabled = false; | |
progressBar.style.width = '0%'; | |
progressPercent.textContent = '0%'; | |
fileInput.value = ''; | |
description.value = ''; | |
dropzone.innerHTML = ` | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /> | |
</svg> | |
<p class="mt-2 text-gray-600">Drag & drop files here or click to browse</p> | |
`; | |
// Reload gallery | |
loadGalleryItems(); | |
}, 1000); | |
} catch (error) { | |
console.error('Upload error:', error); | |
alert('Error uploading files. Please try again.'); | |
uploadProgress.classList.add('hidden'); | |
uploadBtn.disabled = false; | |
} | |
} | |
async function loadGalleryItems() { | |
try { | |
galleryContainer.innerHTML = '<div class="col-span-full text-center py-8">Loading...</div>'; | |
// Calculate range for pagination | |
const from = (currentPage - 1) * itemsPerPage; | |
const to = from + itemsPerPage - 1; | |
// Build query based on filter | |
let query = supabase | |
.from('gallery') | |
.select('*', { count: 'exact' }) | |
.order('created_at', { ascending: false }) | |
.range(from, to); | |
if (currentFilter !== 'all') { | |
query = query.eq('type', currentFilter); | |
} | |
const { data: items, count, error } = await query; | |
if (error) throw error; | |
// Update gallery display | |
if (items && items.length > 0) { | |
emptyGalleryMessage.classList.add('hidden'); | |
renderGalleryItems(items); | |
// Update pagination | |
const totalPages = Math.ceil(count / itemsPerPage); | |
pageInfo.textContent = `Page ${currentPage} of ${totalPages}`; | |
prevPage.disabled = currentPage <= 1; | |
nextPage.disabled = currentPage >= totalPages; | |
if (totalPages > 1) { | |
pagination.classList.remove('hidden'); | |
} else { | |
pagination.classList.add('hidden'); | |
} | |
} else { | |
galleryContainer.innerHTML = ''; | |
emptyGalleryMessage.classList.remove('hidden'); | |
pagination.classList.add('hidden'); | |
} | |
} catch (error) { | |
console.error('Error loading gallery items:', error); | |
galleryContainer.innerHTML = '<div class="col-span-full text-center py-8 text-red-500">Error loading gallery. Please try again.</div>'; | |
} | |
} | |
function renderGalleryItems(items) { | |
galleryContainer.innerHTML = ''; | |
items.forEach(item => { | |
const galleryItem = document.createElement('div'); | |
galleryItem.className = 'gallery-item bg-white rounded-lg overflow-hidden shadow-sm border border-gray-100 cursor-pointer'; | |
galleryItem.addEventListener('click', () => openMediaModal(item)); | |
if (item.type === 'image') { | |
galleryItem.innerHTML = ` | |
<img src="${item.url}" alt="${item.description || 'Gallery image'}" class="w-full h-48 object-cover"> | |
<div class="p-3"> | |
<p class="text-sm text-gray-700 truncate">${item.description || 'No description'}</p> | |
<p class="text-xs text-gray-500 mt-1">${formatDate(item.created_at)}</p> | |
</div> | |
`; | |
} else { | |
galleryItem.innerHTML = ` | |
<div class="relative w-full h-48 bg-gray-200 flex items-center justify-center"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" /> | |
</svg> | |
<div class="absolute bottom-2 right-2 bg-black bg-opacity-50 text-white text-xs px-1 rounded">VIDEO</div> | |
</div> | |
<div class="p-3"> | |
<p class="text-sm text-gray-700 truncate">${item.description || 'No description'}</p> | |
<p class="text-xs text-gray-500 mt-1">${formatDate(item.created_at)}</p> | |
</div> | |
`; | |
} | |
galleryContainer.appendChild(galleryItem); | |
}); | |
} | |
function openMediaModal(item) { | |
modalTitle.textContent = item.description || (item.type === 'image' ? 'Image' : 'Video'); | |
modalDescription.textContent = item.description || 'No description provided'; | |
modalDate.textContent = formatDate(item.created_at, true); | |
modalContent.innerHTML = ''; | |
if (item.type === 'image') { | |
const img = document.createElement('img'); | |
img.src = item.url; | |
img.alt = item.description || 'Gallery image'; | |
img.className = 'max-w-full max-h-[70vh] object-contain'; | |
modalContent.appendChild(img); | |
} else { | |
const videoContainer = document.createElement('div'); | |
videoContainer.className = 'w-full'; | |
videoContainer.innerHTML = ` | |
<video controls class="max-w-full max-h-[70vh]"> | |
<source src="${item.url}" type="video/mp4"> | |
Your browser does not support the video tag. | |
</video> | |
`; | |
modalContent.appendChild(videoContainer); | |
} | |
mediaModal.classList.remove('hidden'); | |
} | |
function formatDate(dateString, full = false) { | |
const date = new Date(dateString); | |
if (full) { | |
return date.toLocaleString(); | |
} | |
return date.toLocaleDateString(); | |
} | |
</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=Dagfinn1962/gallery202508" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |