Spaces:
Running
Running
<html lang="en" class="dark"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Cinematic Photo Gallery</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://accounts.google.com/gsi/client" async defer></script> | |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
<style> | |
@keyframes fadeIn { | |
from { opacity: 0; } | |
to { opacity: 1; } | |
} | |
@keyframes slideUp { | |
from { transform: translateY(20px); opacity: 0; } | |
to { transform: translateY(0); opacity: 1; } | |
} | |
.animate-fade-in { animation: fadeIn 0.3s ease-out forwards; } | |
.animate-slide-up { animation: slideUp 0.3s ease-out forwards; } | |
.flipbook { | |
perspective: 2000px; | |
} | |
.page { | |
transform-style: preserve-3d; | |
transform-origin: left center; | |
transition: transform 0.8s; | |
backface-visibility: hidden; | |
} | |
.page.flipped { | |
transform: rotateY(-180deg); | |
} | |
.page-content { | |
backface-visibility: hidden; | |
} | |
.page-back { | |
transform: rotateY(180deg); | |
backface-visibility: hidden; | |
} | |
.blur-bg { | |
backdrop-filter: blur(10px); | |
background-color: rgba(0, 0, 0, 0.7); | |
} | |
.album-card:hover .album-overlay { | |
opacity: 1; | |
} | |
.drag-active { | |
border-color: #3b82f6 ; | |
background-color: rgba(59, 130, 246, 0.1) ; | |
} | |
</style> | |
</head> | |
<body class="font-inter bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen transition-colors duration-300"> | |
<!-- Navigation --> | |
<nav class="bg-white dark:bg-gray-800 shadow-sm sticky top-0 z-50"> | |
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> | |
<div class="flex justify-between h-16"> | |
<div class="flex items-center"> | |
<a href="#" class="text-xl font-bold text-indigo-600 dark:text-indigo-400">PhotoGallery</a> | |
</div> | |
<div class="flex items-center space-x-4"> | |
<button id="darkModeToggle" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 hidden dark:block" viewBox="0 0 20 20" fill="currentColor"> | |
<path fill-rule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clip-rule="evenodd" /> | |
</svg> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 block dark:hidden" viewBox="0 0 20 20" fill="currentColor"> | |
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /> | |
</svg> | |
</button> | |
<button id="loginBtn" class="px-4 py-2 rounded-md bg-indigo-600 text-white hover:bg-indigo-700 transition">Login</button> | |
<button id="adminBtn" class="px-4 py-2 rounded-md bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition hidden">Admin</button> | |
</div> | |
</div> | |
</div> | |
</nav> | |
<!-- Main Content --> | |
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> | |
<!-- Homepage --> | |
<div id="homepage"> | |
<div class="flex justify-between items-center mb-8"> | |
<h1 class="text-3xl font-bold">Photo Albums</h1> | |
<div class="flex space-x-2"> | |
<button id="recentlyBtn" class="px-4 py-2 rounded-md bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition">Recently</button> | |
</div> | |
</div> | |
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> | |
<!-- Album Cards --> | |
<div class="album-card bg-white dark:bg-gray-800 rounded-xl shadow-md overflow-hidden transition-transform hover:scale-105 cursor-pointer relative" data-album-id="1" data-visibility="public"> | |
<div class="relative aspect-square"> | |
<img src="https://source.unsplash.com/random/600x600/?nature,1" alt="Nature" class="w-full h-full object-cover"> | |
<div class="album-overlay absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center opacity-0 transition-opacity"> | |
<span class="text-white font-medium">View Album</span> | |
</div> | |
</div> | |
<div class="p-4"> | |
<h3 class="font-semibold">Nature</h3> | |
<p class="text-sm text-gray-500 dark:text-gray-400">24 photos</p> | |
<span class="absolute top-2 right-2 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 text-xs px-2 py-1 rounded-full">Public</span> | |
</div> | |
</div> | |
<div class="album-card bg-white dark:bg-gray-800 rounded-xl shadow-md overflow-hidden transition-transform hover:scale-105 cursor-pointer relative" data-album-id="2" data-visibility="family"> | |
<div class="relative aspect-square"> | |
<img src="https://source.unsplash.com/random/600x600/?travel,1" alt="Travel" class="w-full h-full object-cover"> | |
<div class="album-overlay absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center opacity-0 transition-opacity"> | |
<span class="text-white font-medium">View Album</span> | |
</div> | |
</div> | |
<div class="p-4"> | |
<h3 class="font-semibold">Travel</h3> | |
<p class="text-sm text-gray-500 dark:text-gray-400">18 photos</p> | |
<span class="absolute top-2 right-2 bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 text-xs px-2 py-1 rounded-full">Family</span> | |
</div> | |
</div> | |
<div class="album-card bg-white dark:bg-gray-800 rounded-xl shadow-md overflow-hidden transition-transform hover:scale-105 cursor-pointer relative" data-album-id="3" data-visibility="private"> | |
<div class="relative aspect-square"> | |
<img src="https://source.unsplash.com/random/600x600/?portrait,1" alt="Portraits" class="w-full h-full object-cover"> | |
<div class="album-overlay absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center opacity-0 transition-opacity"> | |
<span class="text-white font-medium">View Album</span> | |
</div> | |
</div> | |
<div class="p-4"> | |
<h3 class="font-semibold">Portraits</h3> | |
<p class="text-sm text-gray-500 dark:text-gray-400">12 photos</p> | |
<span class="absolute top-2 right-2 bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200 text-xs px-2 py-1 rounded-full">Private</span> | |
</div> | |
</div> | |
<div class="album-card bg-white dark:bg-gray-800 rounded-xl shadow-md overflow-hidden transition-transform hover:scale-105 cursor-pointer relative" data-album-id="4" data-visibility="public"> | |
<div class="relative aspect-square"> | |
<img src="https://source.unsplash.com/random/600x600/?architecture,1" alt="Architecture" class="w-full h-full object-cover"> | |
<div class="album-overlay absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center opacity-0 transition-opacity"> | |
<span class="text-white font-medium">View Album</span> | |
</div> | |
</div> | |
<div class="p-4"> | |
<h3 class="font-semibold">Architecture</h3> | |
<p class="text-sm text-gray-500 dark:text-gray-400">32 photos</p> | |
<span class="absolute top-2 right-2 bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 text-xs px-2 py-1 rounded-full">Public</span> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Album Viewer --> | |
<div id="albumViewer" class="hidden fixed inset-0 bg-black bg-opacity-90 z-50 flex items-center justify-center p-4"> | |
<div class="relative w-full max-w-4xl"> | |
<button id="closeAlbum" class="absolute -top-12 right-0 text-white hover:text-gray-300"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" 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 class="flipbook bg-white dark:bg-gray-800 rounded-xl shadow-xl overflow-hidden"> | |
<div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700"> | |
<h2 id="albumTitle" class="text-xl font-bold">Album Title</h2> | |
<div class="flex space-x-2"> | |
<span id="albumVisibility" class="px-2 py-1 rounded-full text-xs font-medium">Public</span> | |
<span id="albumPhotoCount" class="text-gray-500 dark:text-gray-400 text-sm">24 photos</span> | |
</div> | |
</div> | |
<div class="relative h-96 md:h-[32rem] overflow-hidden"> | |
<div id="flipbookPages" class="relative h-full w-full flex"> | |
<!-- Pages will be inserted here by JavaScript --> | |
</div> | |
<button id="prevPage" class="absolute left-4 top-1/2 -translate-y-1/2 bg-black bg-opacity-50 text-white rounded-full p-2 hover:bg-opacity-70 disabled:opacity-30 disabled:cursor-not-allowed"> | |
<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="M15 19l-7-7 7-7" /> | |
</svg> | |
</button> | |
<button id="nextPage" class="absolute right-4 top-1/2 -translate-y-1/2 bg-black bg-opacity-50 text-white rounded-full p-2 hover:bg-opacity-70 disabled:opacity-30 disabled:cursor-not-allowed"> | |
<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="M9 5l7 7-7 7" /> | |
</svg> | |
</button> | |
</div> | |
<div class="flex items-center justify-between p-4 border-t border-gray-200 dark:border-gray-700"> | |
<span id="currentPage" class="text-sm text-gray-500 dark:text-gray-400">Page 1 of 5</span> | |
<div class="flex space-x-2"> | |
<button id="zoomOutBtn" class="p-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7" /> | |
</svg> | |
</button> | |
<button id="downloadBtn" class="p-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /> | |
</svg> | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Image Modal --> | |
<div id="imageModal" class="hidden fixed inset-0 bg-black bg-opacity-90 z-50 flex items-center justify-center p-4"> | |
<div class="relative max-w-5xl max-h-screen"> | |
<button id="closeModal" class="absolute -top-12 right-0 text-white hover:text-gray-300"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" 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 class="bg-white dark:bg-gray-800 rounded-xl shadow-xl overflow-hidden"> | |
<div id="modalImageContainer" class="max-h-[80vh] overflow-auto"> | |
<img id="modalImage" src="" alt="" class="w-full h-auto"> | |
</div> | |
<div class="flex items-center justify-between p-4 border-t border-gray-200 dark:border-gray-700"> | |
<h3 id="modalImageTitle" class="font-medium">Image Title</h3> | |
<div class="flex space-x-2"> | |
<button id="modalZoomOut" class="p-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM13 10H7" /> | |
</svg> | |
</button> | |
<button id="modalDownload" class="p-2 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /> | |
</svg> | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Login Modal --> | |
<div id="loginModal" class="hidden fixed inset-0 bg-black bg-opacity-90 z-50 flex items-center justify-center p-4"> | |
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl overflow-hidden w-full max-w-md animate-slide-up"> | |
<div class="p-6"> | |
<div class="flex justify-between items-center mb-6"> | |
<h2 class="text-2xl font-bold">Login</h2> | |
<button id="closeLoginModal" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"> | |
<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="space-y-4"> | |
<div id="g_id_onload" | |
data-client_id="YOUR_GOOGLE_CLIENT_ID" | |
data-context="signin" | |
data-ux_mode="popup" | |
data-callback="handleGoogleSignIn" | |
data-auto_prompt="false"> | |
</div> | |
<div class="g_id_signin" | |
data-type="standard" | |
data-shape="rectangular" | |
data-theme="outline" | |
data-text="signin_with" | |
data-size="large" | |
data-logo_alignment="left"> | |
</div> | |
<div class="flex items-center my-4"> | |
<div class="flex-grow border-t border-gray-300 dark:border-gray-600"></div> | |
<span class="mx-4 text-gray-500 dark:text-gray-400">or</span> | |
<div class="flex-grow border-t border-gray-300 dark:border-gray-600"></div> | |
</div> | |
<div> | |
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label> | |
<input type="email" id="email" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
</div> | |
<div> | |
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label> | |
<input type="password" id="password" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
</div> | |
<div class="flex items-center justify-between"> | |
<div class="flex items-center"> | |
<input id="remember-me" name="remember-me" type="checkbox" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 dark:border-gray-600 rounded dark:bg-gray-700"> | |
<label for="remember-me" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">Remember me</label> | |
</div> | |
<a href="#" class="text-sm text-indigo-600 dark:text-indigo-400 hover:text-indigo-500 dark:hover:text-indigo-300">Forgot password?</a> | |
</div> | |
<button id="emailLoginBtn" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | |
Sign in | |
</button> | |
<div class="text-center text-sm text-gray-500 dark:text-gray-400"> | |
Don't have an account? <a href="#" id="signupLink" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-500 dark:hover:text-indigo-300">Sign up</a> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Signup Modal --> | |
<div id="signupModal" class="hidden fixed inset-0 bg-black bg-opacity-90 z-50 flex items-center justify-center p-4"> | |
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-xl overflow-hidden w-full max-w-md animate-slide-up"> | |
<div class="p-6"> | |
<div class="flex justify-between items-center mb-6"> | |
<h2 class="text-2xl font-bold">Create Account</h2> | |
<button id="closeSignupModal" class="text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"> | |
<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="space-y-4"> | |
<div> | |
<label for="signupName" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Full Name</label> | |
<input type="text" id="signupName" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
</div> | |
<div> | |
<label for="signupEmail" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label> | |
<input type="email" id="signupEmail" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
</div> | |
<div> | |
<label for="signupPassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Password</label> | |
<input type="password" id="signupPassword" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
</div> | |
<div> | |
<label for="signupConfirmPassword" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Confirm Password</label> | |
<input type="password" id="signupConfirmPassword" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
</div> | |
<div class="flex items-center"> | |
<input id="terms" name="terms" type="checkbox" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 dark:border-gray-600 rounded dark:bg-gray-700"> | |
<label for="terms" class="ml-2 block text-sm text-gray-700 dark:text-gray-300"> | |
I agree to the <a href="#" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-500 dark:hover:text-indigo-300">Terms and Conditions</a> | |
</label> | |
</div> | |
<button id="signupBtn" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | |
Create Account | |
</button> | |
<div class="text-center text-sm text-gray-500 dark:text-gray-400"> | |
Already have an account? <a href="#" id="loginLink" class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-500 dark:hover:text-indigo-300">Log in</a> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<!-- Recently View --> | |
<div id="recentlyView" class="hidden"> | |
<div class="flex justify-between items-center mb-8"> | |
<h1 class="text-3xl font-bold">Recently Viewed</h1> | |
<button id="backToHome" class="px-4 py-2 rounded-md bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition">Back to Albums</button> | |
</div> | |
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6"> | |
<!-- Recently viewed photos will be inserted here by JavaScript --> | |
</div> | |
</div> | |
<!-- Admin Dashboard --> | |
<div id="adminDashboard" class="hidden"> | |
<div class="flex justify-between items-center mb-8"> | |
<h1 class="text-3xl font-bold">Admin Dashboard</h1> | |
<div class="flex space-x-2"> | |
<button id="backToHomeAdmin" class="px-4 py-2 rounded-md bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition">Back to Albums</button> | |
</div> | |
</div> | |
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> | |
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6"> | |
<h2 class="text-xl font-semibold mb-4">Upload Photos</h2> | |
<div id="dropZone" class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-8 text-center cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto 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="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-sm text-gray-500 dark:text-gray-400">Drag and drop files here</p> | |
<p class="text-xs text-gray-400 dark:text-gray-500">or</p> | |
<button id="selectFilesBtn" class="mt-2 px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition">Select Files</button> | |
<input type="file" id="fileInput" class="hidden" multiple accept="image/*"> | |
</div> | |
<div id="uploadProgress" class="mt-4 hidden"> | |
<div class="flex justify-between mb-1"> | |
<span class="text-sm font-medium">Uploading...</span> | |
<span id="uploadPercentage" class="text-sm font-medium">0%</span> | |
</div> | |
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5"> | |
<div id="progressBar" class="bg-indigo-600 h-2.5 rounded-full" style="width: 0%"></div> | |
</div> | |
</div> | |
<div id="uploadedFiles" class="mt-4 space-y-2 hidden"> | |
<h3 class="font-medium">Uploaded Files</h3> | |
<div id="uploadedFilesList" class="space-y-2"> | |
<!-- Uploaded files will be listed here --> | |
</div> | |
</div> | |
<div class="mt-6"> | |
<label for="albumSelect" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Add to Album</label> | |
<select id="albumSelect" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
<option value="">Select an album</option> | |
<option value="1">Nature</option> | |
<option value="2">Travel</option> | |
<option value="3">Portraits</option> | |
<option value="4">Architecture</option> | |
<option value="new">Create New Album</option> | |
</select> | |
<div id="newAlbumFields" class="mt-2 hidden"> | |
<input type="text" id="newAlbumName" placeholder="Album Name" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white mb-2"> | |
<select id="newAlbumVisibility" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
<option value="public">Public</option> | |
<option value="family">Family</option> | |
<option value="private">Private</option> | |
</select> | |
</div> | |
<div class="mt-4"> | |
<label for="photoTitles" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Photo Titles (comma separated)</label> | |
<input type="text" id="photoTitles" placeholder="Title 1, Title 2, Title 3" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
</div> | |
<div class="mt-4"> | |
<label for="photoTags" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Tags (comma separated)</label> | |
<input type="text" id="photoTags" placeholder="nature, outdoor, summer" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
</div> | |
<button id="savePhotosBtn" class="mt-4 w-full py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"> | |
Save Photos | |
</button> | |
</div> | |
</div> | |
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6"> | |
<h2 class="text-xl font-semibold mb-4">User Management</h2> | |
<div class="overflow-x-auto"> | |
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> | |
<thead class="bg-gray-50 dark:bg-gray-700"> | |
<tr> | |
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Name</th> | |
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Email</th> | |
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Role</th> | |
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th> | |
</tr> | |
</thead> | |
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"> | |
<tr> | |
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">John Doe</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">john@example.com</td> | |
<td class="px-6 py-4 whitespace-nowrap"> | |
<select class="text-sm border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
<option>Admin</option> | |
<option selected>Viewer</option> | |
</select> | |
</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"> | |
<button class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">Lock</button> | |
</td> | |
</tr> | |
<tr> | |
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">Jane Smith</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">jane@example.com</td> | |
<td class="px-6 py-4 whitespace-nowrap"> | |
<select class="text-sm border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
<option selected>Admin</option> | |
<option>Viewer</option> | |
</select> | |
</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"> | |
<button class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">Lock</button> | |
</td> | |
</tr> | |
<tr> | |
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">Bob Johnson</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">bob@example.com</td> | |
<td class="px-6 py-4 whitespace-nowrap"> | |
<select class="text-sm border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
<option>Admin</option> | |
<option selected>Viewer</option> | |
</select> | |
</td> | |
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400"> | |
<button class="text-indigo-600 dark:text-indigo-400 hover:text-indigo-900 dark:hover:text-indigo-300">Lock</button> | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
<div class="mt-4"> | |
<h3 class="font-medium mb-2">Add New User</h3> | |
<div class="flex space-x-2"> | |
<input type="email" placeholder="Email" class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
<select class="text-sm border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
<option>Admin</option> | |
<option>Viewer</option> | |
</select> | |
<button class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition">Add</button> | |
</div> | |
</div> | |
</div> | |
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-md p-6"> | |
<h2 class="text-xl font-semibold mb-4">Album Management</h2> | |
<div class="space-y-4"> | |
<div class="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"> | |
<div> | |
<h3 class="font-medium">Nature</h3> | |
<p class="text-sm text-gray-500 dark:text-gray-400">24 photos</p> | |
</div> | |
<div class="flex items-center space-x-2"> | |
<select class="text-xs border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
<option value="public" selected>Public</option> | |
<option value="family">Family</option> | |
<option value="private">Private</option> | |
</select> | |
<button class="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> | |
</svg> | |
</button> | |
</div> | |
</div> | |
<div class="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"> | |
<div> | |
<h3 class="font-medium">Travel</h3> | |
<p class="text-sm text-gray-500 dark:text-gray-400">18 photos</p> | |
</div> | |
<div class="flex items-center space-x-2"> | |
<select class="text-xs border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
<option value="public">Public</option> | |
<option value="family" selected>Family</option> | |
<option value="private">Private</option> | |
</select> | |
<button class="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> | |
</svg> | |
</button> | |
</div> | |
</div> | |
<div class="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"> | |
<div> | |
<h3 class="font-medium">Portraits</h3> | |
<p class="text-sm text-gray-500 dark:text-gray-400">12 photos</p> | |
</div> | |
<div class="flex items-center space-x-2"> | |
<select class="text-xs border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
<option value="public">Public</option> | |
<option value="family">Family</option> | |
<option value="private" selected>Private</option> | |
</select> | |
<button class="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> | |
</svg> | |
</button> | |
</div> | |
</div> | |
<div class="flex justify-between items-center p-3 bg-gray-50 dark:bg-gray-700 rounded-lg"> | |
<div> | |
<h3 class="font-medium">Architecture</h3> | |
<p class="text-sm text-gray-500 dark:text-gray-400">32 photos</p> | |
</div> | |
<div class="flex items-center space-x-2"> | |
<select class="text-xs border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
<option value="public" selected>Public</option> | |
<option value="family">Family</option> | |
<option value="private">Private</option> | |
</select> | |
<button class="p-1 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /> | |
</svg> | |
</button> | |
</div> | |
</div> | |
</div> | |
<div class="mt-4"> | |
<h3 class="font-medium mb-2">Create New Album</h3> | |
<div class="flex space-x-2"> | |
<input type="text" placeholder="Album Name" class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
<select class="text-sm border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 dark:bg-gray-700 dark:text-white"> | |
<option value="public">Public</option> | |
<option value="family">Family</option> | |
<option value="private">Private</option> | |
</select> | |
<button class="px-4 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700 transition">Create</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</main> | |
<script> | |
// Dark mode toggle | |
document.getElementById('darkModeToggle').addEventListener('click', function() { | |
document.documentElement.classList.toggle('dark'); | |
localStorage.setItem('darkMode', document.documentElement.classList.contains('dark')); | |
}); | |
// Check for saved dark mode preference | |
if (localStorage.getItem('darkMode') === 'true') { | |
document.documentElement.classList.add('dark'); | |
} else if (localStorage.getItem('darkMode') === 'false') { | |
document.documentElement.classList.remove('dark'); | |
} else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
document.documentElement.classList.add('dark'); | |
} | |
// Modal management | |
const loginModal = document.getElementById('loginModal'); | |
const signupModal = document.getElementById('signupModal'); | |
const albumViewer = document.getElementById('albumViewer'); | |
const imageModal = document.getElementById('imageModal'); | |
const homepage = document.getElementById('homepage'); | |
const recentlyView = document.getElementById('recentlyView'); | |
const adminDashboard = document.getElementById('adminDashboard'); | |
// Login/Signup modal toggles | |
document.getElementById('loginBtn').addEventListener('click', () => { | |
loginModal.classList.remove('hidden'); | |
loginModal.classList.add('animate-fade-in'); | |
}); | |
document.getElementById('closeLoginModal').addEventListener('click', () => { | |
loginModal.classList.add('hidden'); | |
}); | |
document.getElementById('signupLink').addEventListener('click', (e) => { | |
e.preventDefault(); | |
loginModal.classList.add('hidden'); | |
signupModal.classList.remove('hidden'); | |
signupModal.classList.add('animate-fade-in'); | |
}); | |
document.getElementById('closeSignupModal').addEventListener('click', () => { | |
signupModal.classList.add('hidden'); | |
}); | |
document.getElementById('loginLink').addEventListener('click', (e) => { | |
e.preventDefault(); | |
signupModal.classList.add('hidden'); | |
loginModal.classList.remove('hidden'); | |
loginModal.classList.add('animate-fade-in'); | |
}); | |
// Mock login functionality | |
document.getElementById('emailLoginBtn').addEventListener('click', () => { | |
// In a real app, you would validate credentials here | |
localStorage.setItem('isLoggedIn', 'true'); | |
localStorage.setItem('isAdmin', 'true'); // For demo purposes, always log in as admin | |
document.getElementById('loginBtn').classList.add('hidden'); | |
document.getElementById('adminBtn').classList.remove('hidden'); | |
loginModal.classList.add('hidden'); | |
}); | |
// Mock signup functionality | |
document.getElementById('signupBtn').addEventListener('click', () => { | |
// In a real app, you would create a new user account here | |
alert('Account created successfully! Please log in.'); | |
signupModal.classList.add('hidden'); | |
loginModal.classList.remove('hidden'); | |
loginModal.classList.add('animate-fade-in'); | |
}); | |
// Google Sign-In callback | |
function handleGoogleSignIn(response) { | |
console.log('Google sign-in response:', response); | |
// In a real app, you would verify the credential and log the user in | |
localStorage.setItem('isLoggedIn', 'true'); | |
localStorage.setItem('isAdmin', 'true'); // For demo purposes, always log in as admin | |
document.getElementById('loginBtn').classList.add('hidden'); | |
document.getElementById('adminBtn').classList.remove('hidden'); | |
loginModal.classList.add('hidden'); | |
} | |
// Check if user is logged in on page load | |
if (localStorage.getItem('isLoggedIn') === 'true') { | |
document.getElementById('loginBtn').classList.add('hidden'); | |
document.getElementById('adminBtn').classList.remove('hidden'); | |
} | |
// Admin dashboard toggle | |
document.getElementById('adminBtn').addEventListener('click', () => { | |
homepage.classList.add('hidden'); | |
recentlyView.classList.add('hidden'); | |
adminDashboard.classList.remove('hidden'); | |
}); | |
// Back to home buttons | |
document.getElementById('backToHome').addEventListener('click', () => { | |
recentlyView.classList.add('hidden'); | |
homepage.classList.remove('hidden'); | |
}); | |
document.getElementById('backToHomeAdmin').addEventListener('click', () => { | |
adminDashboard.classList.add('hidden'); | |
homepage.classList.remove('hidden'); | |
}); | |
// Recently view toggle | |
document.getElementById('recentlyBtn').addEventListener('click', () => { | |
homepage.classList.add('hidden'); | |
adminDashboard.classList.add('hidden'); | |
recentlyView.classList.remove('hidden'); | |
// In a real app, you would fetch recently viewed photos here | |
const recentlyGrid = recentlyView.querySelector('.grid'); | |
recentlyGrid.innerHTML = ''; | |
// Mock recently viewed photos | |
const recentPhotos = [ | |
{ id: 1, title: 'Mountain View', url: 'https://source.unsplash.com/random/600x600/?mountain,1', views: 24, tags: ['nature', 'landscape'] }, | |
{ id: 2, title: 'Ocean Sunset', url: 'https://source.unsplash.com/random/600x600/?ocean,1', views: 18, tags: ['nature', 'water'] }, | |
{ id: 3, title: 'City Skyline', url: 'https://source.unsplash.com/random/600x600/?city,1', views: 12, tags: ['urban', 'architecture'] }, | |
{ id: 4, title: 'Forest Path', url: 'https://source.unsplash.com/random/600x600/?forest,1', views: 8, tags: ['nature', 'trees'] } | |
]; | |
recentPhotos.forEach(photo => { | |
const photoCard = document.createElement('div'); | |
photoCard.className = 'bg-white dark:bg-gray-800 rounded-xl shadow-md overflow-hidden transition-transform hover:scale-105 cursor-pointer'; | |
photoCard.innerHTML = ` | |
<div class="relative aspect-square"> | |
<img src="${photo.url}" alt="${photo.title}" class="w-full h-full object-cover"> | |
</div> | |
<div class="p-4"> | |
<h3 class="font-semibold">${photo.title}</h3> | |
<div class="flex justify-between items-center mt-2"> | |
<p class="text-sm text-gray-500 dark:text-gray-400">${photo.views} views</p> | |
<div class="flex space-x-1"> | |
${photo.tags.map(tag => `<span class="text-xs bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded-full">${tag}</span>`).join('')} | |
</div> | |
</div> | |
</div> | |
`; | |
recentlyGrid.appendChild(photoCard); | |
}); | |
}); | |
// Album viewer functionality | |
const albumCards = document.querySelectorAll('.album-card'); | |
albumCards.forEach(card => { | |
card.addEventListener('click', function() { | |
const albumId = this.getAttribute('data-album-id'); | |
const visibility = this.getAttribute('data-visibility'); | |
// Check if album is private and user is not logged in | |
if (visibility === 'private' && localStorage.getItem('isLoggedIn') !== 'true') { | |
alert('This album is private. Please log in to view.'); | |
return; | |
} | |
// Check if album is family-only and user is not logged in | |
if (visibility === 'family' && localStorage.getItem('isLoggedIn') !== 'true') { | |
alert('This album is for family members only. Please log in to view.'); | |
return; | |
} | |
// Set album title and visibility | |
const albumTitle = this.querySelector('h3').textContent; | |
const photoCount = this.querySelector('p').textContent; | |
document.getElementById('albumTitle').textContent = albumTitle; | |
document.getElementById('albumPhotoCount').textContent = photoCount; | |
const visibilitySpan = document.getElementById('albumVisibility'); | |
visibilitySpan.textContent = visibility.charAt(0).toUpperCase() + visibility.slice(1); | |
visibilitySpan.className = 'px-2 py-1 rounded-full text-xs font-medium ' + | |
(visibility === 'public' ? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200' : | |
visibility === 'family' ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200' : | |
'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200'); | |
// Show album viewer | |
homepage.classList.add('hidden'); | |
albumViewer.classList.remove('hidden'); | |
// Load album photos | |
loadAlbumPhotos(albumId); | |
}); | |
}); | |
function loadAlbumPhotos(albumId) { | |
const flipbookPages = document.getElementById('flipbookPages'); | |
flipbookPages.innerHTML = ''; | |
// In a real app, you would fetch photos for this album from a database | |
// Here we're using mock data | |
const photos = []; | |
const photoCount = albumId === '1' ? 24 : albumId === '2' ? 18 : albumId === '3' ? 12 : 32; | |
for (let i = 1; i <= photoCount; i++) { | |
photos.push({ | |
id: i, | |
title: `Photo ${i}`, | |
url: `https://source.unsplash.com/random/800x600/?${albumId === '1' ? 'nature' : albumId === '2' ? 'travel' : albumId === '3' ? 'portrait' : 'architecture'},${i}`, | |
tags: ['sample', 'tag'] | |
}); | |
} | |
// Create pages with 4 photos each (2 rows of 2) | |
let currentPage = 1; | |
const photosPerPage = 4; | |
const totalPages = Math.ceil(photos.length / photosPerPage); | |
document.getElementById('currentPage').textContent = `Page 1 of ${totalPages}`; | |
// Create first page | |
createPage(photos, 0, photosPerPage, currentPage, totalPages); | |
// Navigation buttons | |
document.getElementById('prevPage').addEventListener('click', () => { | |
if (currentPage > 1) { | |
currentPage--; | |
createPage(photos, (currentPage - 1) * photosPerPage, currentPage * photosPerPage, currentPage, totalPages); | |
document.getElementById('currentPage').textContent = `Page ${currentPage} of ${totalPages}`; | |
} | |
}); | |
document.getElementById('nextPage').addEventListener('click', () => { | |
if (currentPage < totalPages) { | |
currentPage++; | |
createPage(photos, (currentPage - 1) * photosPerPage, currentPage * photosPerPage, currentPage, totalPages); | |
document.getElementById('currentPage').textContent = `Page ${currentPage} of ${totalPages}`; | |
} | |
}); | |
function createPage(photos, start, end, currentPage, totalPages) { | |
flipbookPages.innerHTML = ''; | |
const pageDiv = document.createElement('div'); | |
pageDiv.className = 'page absolute inset-0 flex flex-col'; | |
const row1 = document.createElement('div'); | |
row1.className = 'flex-1 grid grid-cols-2 gap-2 p-4'; | |
const row2 = document.createElement('div'); | |
row2.className = 'flex-1 grid grid-cols-2 gap-2 p-4 pt-0'; | |
for (let i = start; i < Math.min(end, photos.length); i++) { | |
const photo = photos[i]; | |
const photoDiv = document.createElement('div'); | |
photoDiv.className = 'relative group cursor-pointer'; | |
photoDiv.innerHTML = ` | |
<img src="${photo.url}" alt="${photo.title}" class="w-full h-full object-cover rounded-lg"> | |
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-20 transition-all duration-300 rounded-lg flex items-center justify-center opacity-0 group-hover:opacity-100"> | |
<span class="text-white font-medium">${photo.title}</span> | |
</div> | |
`; | |
// Add click event to view photo in modal | |
photoDiv.addEventListener('click', () => { | |
document.getElementById('modalImage').src = photo.url; | |
document.getElementById('modalImage').alt = photo.title; | |
document.getElementById('modalImageTitle').textContent = photo.title; | |
imageModal.classList.remove('hidden'); | |
// Track view in localStorage | |
const viewedPhotos = JSON.parse(localStorage.getItem('viewedPhotos') || '[]'); | |
if (!viewedPhotos.some(p => p.id === photo.id)) { | |
viewedPhotos.push({ | |
id: photo.id, | |
title: photo.title, | |
url: photo.url, | |
viewedAt: new Date().toISOString(), | |
tags: photo.tags | |
}); | |
localStorage.setItem('viewedPhotos', JSON.stringify(viewedPhotos)); | |
} | |
}); | |
if (i - start < 2) { | |
row1.appendChild(photoDiv); | |
} else { | |
row2.appendChild(photoDiv); | |
} | |
} | |
pageDiv.appendChild(row1); | |
pageDiv.appendChild(row2); | |
flipbookPages.appendChild(pageDiv); | |
// Disable/enable navigation buttons | |
document.getElementById('prevPage').disabled = currentPage === 1; | |
document.getElementById('nextPage').disabled = currentPage === totalPages; | |
} | |
} | |
// Close album viewer | |
document.getElementById('closeAlbum').addEventListener('click', () => { | |
albumViewer.classList.add('hidden'); | |
homepage.classList.remove('hidden'); | |
}); | |
// Image modal functionality | |
document.getElementById('closeModal').addEventListener('click', () => { | |
imageModal.classList.add('hidden'); | |
}); | |
document.getElementById('modalZoomOut').addEventListener('click', () => { | |
const img = document.getElementById('modalImage'); | |
img.style.transform = img.style.transform === 'scale(0.8)' ? 'scale(1)' : 'scale(0.8)'; | |
}); | |
document.getElementById('modalDownload').addEventListener('click', () => { | |
// In a real app, this would trigger a download | |
alert('Downloading image...'); | |
}); | |
// Admin photo upload functionality | |
const dropZone = document.getElementById('dropZone'); | |
const fileInput = document.getElementById('fileInput'); | |
const selectFilesBtn = document.getElementById('selectFilesBtn'); | |
const uploadProgress = document.getElementById('uploadProgress'); | |
const progressBar = document.getElementById('progressBar'); | |
const uploadPercentage = document.getElementById('uploadPercentage'); | |
const uploadedFiles = document.getElementById('uploadedFiles'); | |
const uploadedFilesList = document.getElementById('uploadedFilesList'); | |
const albumSelect = document.getElementById('albumSelect'); | |
const newAlbumFields = document.getElementById('newAlbumFields'); | |
// Drag and drop events | |
['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('drag-active'); | |
} | |
function unhighlight() { | |
dropZone.classList.remove('drag-active'); | |
} | |
// Handle dropped files | |
dropZone.addEventListener('drop', handleDrop, false); | |
function handleDrop(e) { | |
const dt = e.dataTransfer; | |
const files = dt.files; | |
handleFiles(files); | |
} | |
// Handle selected files | |
selectFilesBtn.addEventListener('click', () => { | |
fileInput.click(); | |
}); | |
fileInput.addEventListener('change', () => { | |
handleFiles(fileInput.files); | |
}); | |
function handleFiles(files) { | |
uploadProgress.classList.remove('hidden'); | |
uploadedFilesList.innerHTML = ''; | |
let progress = 0; | |
const totalFiles = files.length; | |
let processedFiles = 0; | |
// Mock upload progress | |
const interval = setInterval(() => { | |
progress += Math.random() * 10; | |
if (progress >= 100) { | |
progress = 100; | |
clearInterval(interval); | |
// Show uploaded files | |
uploadedFiles.classList.remove('hidden'); | |
Array.from(files).forEach((file, index) => { | |
const fileItem = document.createElement('div'); | |
fileItem.className = 'flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700 rounded'; | |
fileItem.innerHTML = ` | |
<div class="flex items-center space-x-2"> | |
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500 dark:text-gray-400" 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> | |
<span class="text-sm">${file.name}</span> | |
</div> | |
<span class="text-xs text-green-600 dark:text-green-400">Uploaded</span> | |
`; | |
uploadedFilesList.appendChild(fileItem); | |
}); | |
} | |
progressBar.style.width = `${progress}%`; | |
uploadPercentage.textContent = `${Math.round(progress)}%`; | |
}, 200); | |
} | |
// New album fields toggle | |
albumSelect.addEventListener('change', function() { | |
if (this.value === 'new') { | |
newAlbumFields.classList.remove('hidden'); | |
} else { | |
newAlbumFields.classList.add('hidden'); | |
} | |
}); | |
// Save photos button | |
document.getElementById('savePhotosBtn').addEventListener('click', function() { | |
const albumId = albumSelect.value; | |
const albumName = albumId === 'new' ? document.getElementById('newAlbumName').value : albumSelect.options[albumSelect.selectedIndex].text; | |
const albumVisibility = albumId === 'new' ? document.getElementById('newAlbumVisibility').value : ''; | |
const photoTitles = document.getElementById('photoTitles').value.split(','); | |
const photoTags = document.getElementById('photoTags').value.split(','); | |
// In a real app, you would save this data to a database | |
alert(`Photos saved to ${albumName} (${albumVisibility || 'existing album'})`); | |
// Reset form | |
uploadProgress.classList.add('hidden'); | |
uploadedFiles.classList.add('hidden'); | |
fileInput.value = ''; | |
document.getElementById('photoTitles').value = ''; | |
document.getElementById('photoTags').value = ''; | |
albumSelect.value = ''; | |
if (albumId === 'new') { | |
document.getElementById('newAlbumName').value = ''; | |
document.getElementById('newAlbumVisibility').value = 'public'; | |
newAlbumFields.classList.add('hidden'); | |
} | |
}); | |
</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=akbit/lumemgallery" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |