cinematic-gallery / index.html
akbit's picture
Add 2 files
51ba241 verified
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cinematic Gallery</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">
<script>
tailwind.config = {
darkMode: 'class',
theme: {
extend: {
colors: {
primary: {
50: '#f0f9ff',
100: '#e0f2fe',
200: '#bae6fd',
300: '#7dd3fc',
400: '#38bdf8',
500: '#0ea5e9',
600: '#0284c7',
700: '#0369a1',
800: '#075985',
900: '#0c4a6e',
}
},
animation: {
'fade-in': 'fadeIn 0.3s ease-in-out',
'zoom-in': 'zoomIn 0.2s ease-out',
},
keyframes: {
fadeIn: {
'0%': { opacity: '0' },
'100%': { opacity: '1' },
},
zoomIn: {
'0%': { transform: 'scale(0.95)', opacity: '0' },
'100%': { transform: 'scale(1)', opacity: '1' },
}
}
}
}
}
</script>
<style>
.masonry {
column-count: 1;
column-gap: 1rem;
}
@media (min-width: 640px) {
.masonry {
column-count: 2;
}
}
@media (min-width: 1024px) {
.masonry {
column-count: 3;
}
}
@media (min-width: 1280px) {
.masonry {
column-count: 4;
}
}
.masonry-item {
break-inside: avoid;
margin-bottom: 1rem;
}
.blur-backdrop {
backdrop-filter: blur(10px);
background-color: rgba(0, 0, 0, 0.7);
}
.image-hover {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.image-hover:hover {
transform: scale(1.02);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.album-card {
transition: all 0.3s ease;
}
.album-card:hover {
transform: translateY(-5px);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.drag-over {
border: 2px dashed #3b82f6;
background-color: rgba(59, 130, 246, 0.1);
}
</style>
</head>
<body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen transition-colors duration-200">
<!-- 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="#" onclick="showHomePage()" class="flex items-center">
<i class="fas fa-camera-retro text-2xl text-primary-600 dark:text-primary-400 mr-2"></i>
<span class="text-xl font-bold text-primary-600 dark:text-primary-400">Cinematic Gallery</span>
</a>
</div>
<div class="flex items-center space-x-4">
<button id="theme-toggle" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-moon dark:hidden"></i>
<i class="fas fa-sun hidden dark:block"></i>
</button>
<div id="user-menu" class="hidden">
<button id="user-menu-button" class="flex items-center space-x-2">
<img id="user-avatar" class="w-8 h-8 rounded-full" src="https://via.placeholder.com/32" alt="User">
<span id="username" class="hidden md:inline">Guest</span>
</button>
</div>
<button id="login-button" onclick="showLoginPage()" class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-md transition">
Login
</button>
<button id="logout-button" onclick="logout()" class="hidden px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-md transition">
Logout
</button>
<button id="admin-menu-button" class="hidden p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700" onclick="toggleAdminMenu()">
<i class="fas fa-cog"></i>
</button>
<div id="admin-menu" class="hidden absolute right-4 mt-12 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg py-1 z-50">
<a href="#" onclick="showUploadPage()" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700">Upload Photos</a>
<a href="#" onclick="showPhotoManager()" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700">Photo Manager</a>
<a href="#" onclick="showAlbumManager()" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700">Album Manager</a>
<a href="#" onclick="showUserManager()" class="block px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700">User Management</a>
</div>
</div>
</div>
</div>
</nav>
<!-- Main Content Area -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Home Page -->
<div id="home-page">
<h1 class="text-3xl font-bold mb-8">Photo Albums</h1>
<div id="album-filters" class="mb-6 flex flex-wrap gap-2">
<button class="px-4 py-2 bg-primary-600 text-white rounded-full">All</button>
<button class="px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-full">Public</button>
<button class="px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-full">Family</button>
<button class="px-4 py-2 bg-gray-200 dark:bg-gray-700 rounded-full">Private</button>
</div>
<div id="albums-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<!-- Album cards will be loaded here -->
</div>
<div id="loading-albums" class="mt-8 text-center hidden">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary-600"></div>
<p class="mt-2">Loading more albums...</p>
</div>
</div>
<!-- Album Details Page -->
<div id="album-page" class="hidden">
<div class="flex items-center mb-6">
<button onclick="showHomePage()" class="mr-4 p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-arrow-left"></i>
</button>
<h1 id="album-title" class="text-3xl font-bold"></h1>
<span id="album-visibility" class="ml-4 px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-gray-700"></span>
</div>
<div id="album-description" class="mb-8 text-gray-600 dark:text-gray-300"></div>
<div id="photos-masonry" class="masonry">
<!-- Photos will be loaded here in masonry layout -->
</div>
<div id="loading-photos" class="mt-8 text-center hidden">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary-600"></div>
<p class="mt-2">Loading more photos...</p>
</div>
</div>
<!-- Login Page -->
<div id="login-page" class="hidden max-w-md mx-auto py-12">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden">
<div class="p-8">
<h2 class="text-2xl font-bold text-center mb-6">Welcome Back</h2>
<div class="space-y-4">
<button onclick="loginWithGoogle()" class="w-full flex items-center justify-center px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium hover:bg-gray-50 dark:hover:bg-gray-700 transition">
<img src="https://www.google.com/favicon.ico" alt="Google" class="h-5 w-5 mr-2">
Continue with Google
</button>
<div class="relative">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300 dark:border-gray-600"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="px-2 bg-white dark:bg-gray-800 text-gray-500">Or</span>
</div>
</div>
<form id="login-form" class="space-y-4">
<div>
<label for="email" class="block text-sm font-medium">Email</label>
<input type="email" id="email" name="email" required class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
</div>
<div>
<label for="password" class="block text-sm font-medium">Password</label>
<input type="password" id="password" name="password" required class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
</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-primary-600 focus:ring-primary-500 border-gray-300 dark:border-gray-600 rounded">
<label for="remember-me" class="ml-2 block text-sm">Remember me</label>
</div>
<div class="text-sm">
<a href="#" class="font-medium text-primary-600 hover:text-primary-500">Forgot password?</a>
</div>
</div>
<div>
<button type="submit" class="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition">
Sign in
</button>
</div>
</form>
</div>
<div class="mt-6 text-center text-sm">
<p class="text-gray-500">
Don't have an account?
<a href="#" class="font-medium text-primary-600 hover:text-primary-500">Sign up</a>
</p>
</div>
</div>
</div>
</div>
<!-- Upload Page -->
<div id="upload-page" class="hidden">
<div class="flex items-center mb-6">
<button onclick="showHomePage()" class="mr-4 p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-arrow-left"></i>
</button>
<h1 class="text-3xl font-bold">Upload Photos</h1>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8">
<div id="drop-zone" class="border-2 border-dashed border-gray-300 dark:border-gray-600 rounded-lg p-12 text-center cursor-pointer transition">
<div class="flex flex-col items-center justify-center">
<i class="fas fa-cloud-upload-alt text-4xl text-primary-600 mb-4"></i>
<h3 class="text-lg font-medium">Drag and drop photos here</h3>
<p class="text-sm text-gray-500 mt-1">or click to browse files</p>
<input type="file" id="file-input" class="hidden" multiple accept="image/*">
</div>
</div>
<div class="mt-4 flex items-center justify-between">
<div>
<p class="text-sm text-gray-500">Or upload via URL</p>
</div>
<button onclick="addUrlField()" class="px-3 py-1 bg-gray-200 dark:bg-gray-700 rounded-md text-sm hover:bg-gray-300 dark:hover:bg-gray-600 transition">
Add URL
</button>
</div>
<div id="url-fields" class="mt-4 space-y-2">
<!-- URL input fields will be added here -->
</div>
</div>
<div id="upload-progress" class="hidden bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-8">
<h3 class="text-lg font-medium mb-4">Upload Progress</h3>
<div id="progress-container" class="space-y-4">
<!-- Progress bars will be added here -->
</div>
</div>
<div id="uploaded-photos" class="hidden bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<h3 class="text-lg font-medium mb-4">Edit Photo Details</h3>
<div id="photo-edit-forms" class="space-y-6">
<!-- Photo edit forms will be added here -->
</div>
<div class="mt-6 flex justify-end">
<button onclick="saveUploadedPhotos()" class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-md transition">
Save All Photos
</button>
</div>
</div>
</div>
<!-- Photo Manager -->
<div id="photo-manager" class="hidden">
<div class="flex items-center mb-6">
<button onclick="showHomePage()" class="mr-4 p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-arrow-left"></i>
</button>
<h1 class="text-3xl font-bold">Photo Manager</h1>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-6">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex-1 min-w-[200px]">
<label for="search-photos" class="block text-sm font-medium mb-1">Search</label>
<input type="text" id="search-photos" placeholder="Search by title or tags..." class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
</div>
<div class="flex-1 min-w-[200px]">
<label for="filter-album" class="block text-sm font-medium mb-1">Album</label>
<select id="filter-album" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
<option value="">All Albums</option>
<!-- Album options will be added here -->
</select>
</div>
<div class="flex-1 min-w-[200px]">
<label for="filter-status" class="block text-sm font-medium mb-1">Status</label>
<select id="filter-status" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
<option value="">All Status</option>
<option value="active">Active</option>
<option value="disabled">Disabled</option>
</select>
</div>
</div>
</div>
<div id="photos-grid" class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
<!-- Photo cards will be loaded here -->
</div>
<div id="loading-photos-manager" class="mt-8 text-center hidden">
<div class="inline-block animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-primary-600"></div>
<p class="mt-2">Loading more photos...</p>
</div>
</div>
<!-- Album Manager -->
<div id="album-manager" class="hidden">
<div class="flex items-center mb-6">
<button onclick="showHomePage()" class="mr-4 p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-arrow-left"></i>
</button>
<h1 class="text-3xl font-bold">Album Manager</h1>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 mb-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-xl font-medium">Your Albums</h2>
<button onclick="showCreateAlbumModal()" class="px-4 py-2 bg-primary-600 hover:bg-primary-700 text-white rounded-md transition">
<i class="fas fa-plus mr-2"></i> New Album
</button>
</div>
<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">Cover</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">Title</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">Visibility</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">Photos</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 id="album-manager-list" class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<!-- Album rows will be added here -->
</tbody>
</table>
</div>
</div>
</div>
<!-- User Management -->
<div id="user-manager" class="hidden">
<div class="flex items-center mb-6">
<button onclick="showHomePage()" class="mr-4 p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-arrow-left"></i>
</button>
<h1 class="text-3xl font-bold">User Management</h1>
</div>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<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">User</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">Status</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 id="user-manager-list" class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<!-- User rows will be added here -->
</tbody>
</table>
</div>
</div>
</div>
</main>
<!-- Photo Modal -->
<div id="photo-modal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 blur-backdrop opacity-100"></div>
</div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
<div class="absolute top-4 right-4">
<button onclick="closePhotoModal()" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-4">
<div class="flex justify-between items-center mb-4">
<h3 id="photo-modal-title" class="text-lg font-medium"></h3>
<div class="flex space-x-2">
<button id="zoom-out-btn" onclick="zoomOutPhoto()" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-search-minus"></i>
</button>
<button id="zoom-in-btn" onclick="zoomInPhoto()" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-search-plus"></i>
</button>
<button id="download-btn" onclick="downloadPhoto()" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-download"></i>
</button>
</div>
</div>
<div class="overflow-hidden">
<img id="modal-photo" class="mx-auto max-h-[70vh] w-auto rounded-md shadow-md" src="" alt="">
</div>
<div class="mt-4 flex justify-between items-center">
<div id="photo-tags" class="flex flex-wrap gap-2">
<!-- Tags will be added here -->
</div>
<div id="photo-info" class="text-sm text-gray-500">
<!-- Photo info will be added here -->
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Create Album Modal -->
<div id="create-album-modal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 blur-backdrop opacity-100"></div>
</div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div class="absolute top-4 right-4">
<button onclick="closeCreateAlbumModal()" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-6">
<h3 class="text-lg font-medium mb-4">Create New Album</h3>
<form id="create-album-form" class="space-y-4">
<div>
<label for="album-name" class="block text-sm font-medium">Album Name</label>
<input type="text" id="album-name" required class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
</div>
<div>
<label for="album-description" class="block text-sm font-medium">Description</label>
<textarea id="album-description-input" rows="3" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700"></textarea>
</div>
<div>
<label for="album-visibility-select" class="block text-sm font-medium">Visibility</label>
<select id="album-visibility-select" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
<option value="public">Public (Anyone can view)</option>
<option value="family">Family (Only logged-in users can view)</option>
<option value="private">Private (Only admins can view)</option>
</select>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button type="button" onclick="closeCreateAlbumModal()" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition">
Cancel
</button>
<button type="submit" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition">
Create Album
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Edit Album Modal -->
<div id="edit-album-modal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 blur-backdrop opacity-100"></div>
</div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
<div class="absolute top-4 right-4">
<button onclick="closeEditAlbumModal()" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-6">
<h3 class="text-lg font-medium mb-4">Edit Album</h3>
<div class="flex flex-col lg:flex-row gap-6">
<div class="lg:w-1/2">
<form id="edit-album-form" class="space-y-4">
<input type="hidden" id="edit-album-id">
<div>
<label for="edit-album-name" class="block text-sm font-medium">Album Name</label>
<input type="text" id="edit-album-name" required class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
</div>
<div>
<label for="edit-album-description" class="block text-sm font-medium">Description</label>
<textarea id="edit-album-description" rows="3" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700"></textarea>
</div>
<div>
<label for="edit-album-visibility" class="block text-sm font-medium">Visibility</label>
<select id="edit-album-visibility" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
<option value="public">Public (Anyone can view)</option>
<option value="family">Family (Only logged-in users can view)</option>
<option value="private">Private (Only admins can view)</option>
</select>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button type="button" onclick="closeEditAlbumModal()" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition">
Cancel
</button>
<button type="submit" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition">
Save Changes
</button>
</div>
</form>
</div>
<div class="lg:w-1/2">
<div class="mb-4">
<h4 class="font-medium">Select Cover Photo</h4>
<p class="text-sm text-gray-500">Choose a photo from this album to use as the cover</p>
</div>
<div id="album-photos-select" class="grid grid-cols-3 gap-2 max-h-64 overflow-y-auto p-2 bg-gray-100 dark:bg-gray-700 rounded">
<!-- Album photos for selection will be added here -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Photo Modal -->
<div id="edit-photo-modal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 blur-backdrop opacity-100"></div>
</div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-2xl sm:w-full">
<div class="absolute top-4 right-4">
<button onclick="closeEditPhotoModal()" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-6">
<h3 class="text-lg font-medium mb-4">Edit Photo</h3>
<form id="edit-photo-form" class="space-y-4">
<input type="hidden" id="edit-photo-id">
<div class="flex flex-col md:flex-row gap-6">
<div class="md:w-1/2">
<img id="edit-photo-preview" class="w-full h-auto rounded-md shadow-md" src="" alt="">
</div>
<div class="md:w-1/2 space-y-4">
<div>
<label for="edit-photo-title" class="block text-sm font-medium">Title</label>
<input type="text" id="edit-photo-title" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
</div>
<div>
<label for="edit-photo-tags" class="block text-sm font-medium">Tags (comma separated)</label>
<input type="text" id="edit-photo-tags" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
</div>
<div>
<label for="edit-photo-album" class="block text-sm font-medium">Album</label>
<select id="edit-photo-album" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
<!-- Album options will be added here -->
</select>
</div>
<div class="flex items-center">
<input type="checkbox" id="edit-photo-active" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 dark:border-gray-600 rounded">
<label for="edit-photo-active" class="ml-2 block text-sm">Active (visible to users)</label>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button type="button" onclick="closeEditPhotoModal()" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition">
Cancel
</button>
<button type="submit" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition">
Save Changes
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Edit User Modal -->
<div id="edit-user-modal" class="hidden fixed inset-0 z-50 overflow-y-auto">
<div class="flex items-center justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 transition-opacity" aria-hidden="true">
<div class="absolute inset-0 blur-backdrop opacity-100"></div>
</div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg sm:w-full">
<div class="absolute top-4 right-4">
<button onclick="closeEditUserModal()" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="p-6">
<h3 class="text-lg font-medium mb-4">Edit User</h3>
<form id="edit-user-form" class="space-y-4">
<input type="hidden" id="edit-user-id">
<div class="flex items-center space-x-4">
<img id="edit-user-avatar" class="w-16 h-16 rounded-full" src="https://via.placeholder.com/64" alt="">
<div>
<h4 id="edit-user-name" class="font-medium"></h4>
<p id="edit-user-email" class="text-sm text-gray-500"></p>
</div>
</div>
<div>
<label for="edit-user-role" class="block text-sm font-medium">Role</label>
<select id="edit-user-role" class="mt-1 block w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
<option value="admin">Admin</option>
<option value="viewer">Viewer</option>
</select>
</div>
<div class="flex items-center">
<input type="checkbox" id="edit-user-locked" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 dark:border-gray-600 rounded">
<label for="edit-user-locked" class="ml-2 block text-sm">Lock role (prevent changes)</label>
</div>
<div class="flex items-center">
<input type="checkbox" id="edit-user-active" class="h-4 w-4 text-primary-600 focus:ring-primary-500 border-gray-300 dark:border-gray-600 rounded">
<label for="edit-user-active" class="ml-2 block text-sm">Active account</label>
</div>
<div class="flex justify-end space-x-3 pt-4">
<button type="button" onclick="closeEditUserModal()" class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition">
Cancel
</button>
<button type="submit" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 transition">
Save Changes
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<!-- Toast Notifications -->
<div id="toast-container" class="fixed bottom-4 right-4 space-y-2 z-50"></div>
<script>
// State management
const state = {
currentUser: null,
isAdmin: false,
albums: [],
photos: [],
users: [],
currentAlbum: null,
currentPage: 'home',
zoomLevel: 1,
uploadedPhotos: [],
nextAlbumPage: 1,
nextPhotoPage: 1,
hasMoreAlbums: true,
hasMorePhotos: true
};
// DOM Elements
const elements = {
homePage: document.getElementById('home-page'),
albumPage: document.getElementById('album-page'),
loginPage: document.getElementById('login-page'),
uploadPage: document.getElementById('upload-page'),
photoManager: document.getElementById('photo-manager'),
albumManager: document.getElementById('album-manager'),
userManager: document.getElementById('user-manager'),
albumsGrid: document.getElementById('albums-grid'),
photosMasonry: document.getElementById('photos-masonry'),
loadingAlbums: document.getElementById('loading-albums'),
loadingPhotos: document.getElementById('loading-photos'),
albumTitle: document.getElementById('album-title'),
albumDescription: document.getElementById('album-description'),
albumVisibility: document.getElementById('album-visibility'),
userMenu: document.getElementById('user-menu'),
userMenuButton: document.getElementById('user-menu-button'),
loginButton: document.getElementById('login-button'),
logoutButton: document.getElementById('logout-button'),
adminMenuButton: document.getElementById('admin-menu-button'),
adminMenu: document.getElementById('admin-menu'),
photoModal: document.getElementById('photo-modal'),
modalPhoto: document.getElementById('modal-photo'),
photoModalTitle: document.getElementById('photo-modal-title'),
photoTags: document.getElementById('photo-tags'),
photoInfo: document.getElementById('photo-info'),
createAlbumModal: document.getElementById('create-album-modal'),
editAlbumModal: document.getElementById('edit-album-modal'),
editPhotoModal: document.getElementById('edit-photo-modal'),
editUserModal: document.getElementById('edit-user-modal'),
dropZone: document.getElementById('drop-zone'),
fileInput: document.getElementById('file-input'),
urlFields: document.getElementById('url-fields'),
uploadProgress: document.getElementById('upload-progress'),
progressContainer: document.getElementById('progress-container'),
uploadedPhotosContainer: document.getElementById('uploaded-photos'),
photoEditForms: document.getElementById('photo-edit-forms'),
photosGrid: document.getElementById('photos-grid'),
loadingPhotosManager: document.getElementById('loading-photos-manager'),
albumManagerList: document.getElementById('album-manager-list'),
userManagerList: document.getElementById('user-manager-list'),
toastContainer: document.getElementById('toast-container'),
themeToggle: document.getElementById('theme-toggle')
};
// Initialize the app
function init() {
// Load sample data
loadSampleData();
// Check for saved user session
checkUserSession();
// Set up event listeners
setupEventListeners();
// Show home page by default
showHomePage();
// Load initial albums
loadAlbums();
}
// Load sample data for demonstration
function loadSampleData() {
// Sample albums
state.albums = [
{
id: 1,
title: 'Nature Landscapes',
description: 'Beautiful landscapes from around the world',
coverPhoto: 'https://source.unsplash.com/random/600x400/?nature',
visibility: 'public',
photoCount: 12,
createdAt: '2023-05-15'
},
{
id: 2,
title: 'Urban Exploration',
description: 'Cityscapes and urban environments',
coverPhoto: 'https://source.unsplash.com/random/600x400/?city',
visibility: 'public',
photoCount: 8,
createdAt: '2023-06-20'
},
{
id: 3,
title: 'Family Vacation 2023',
description: 'Our summer vacation photos',
coverPhoto: 'https://source.unsplash.com/random/600x400/?vacation',
visibility: 'family',
photoCount: 24,
createdAt: '2023-07-10'
},
{
id: 4,
title: 'Wedding Day',
description: 'Our special day',
coverPhoto: 'https://source.unsplash.com/random/600x400/?wedding',
visibility: 'private',
photoCount: 36,
createdAt: '2023-08-05'
},
{
id: 5,
title: 'Wildlife Photography',
description: 'Animals in their natural habitats',
coverPhoto: 'https://source.unsplash.com/random/600x400/?wildlife',
visibility: 'public',
photoCount: 15,
createdAt: '2023-09-12'
},
{
id: 6,
title: 'Mountain Adventures',
description: 'Hiking and climbing trips',
coverPhoto: 'https://source.unsplash.com/random/600x400/?mountain',
visibility: 'public',
photoCount: 18,
createdAt: '2023-10-08'
}
];
// Sample photos
state.photos = [];
for (let i = 1; i <= 50; i++) {
const albumId = Math.floor(Math.random() * 6) + 1;
state.photos.push({
id: i,
title: `Photo ${i}`,
url: `https://source.unsplash.com/random/800x600/?sig=${i}`,
thumbnailUrl: `https://source.unsplash.com/random/300x200/?sig=${i}`,
albumId: albumId,
tags: ['tag' + (i % 5 + 1), 'tag' + (i % 3 + 1)],
views: Math.floor(Math.random() * 100),
uploadedAt: new Date(Date.now() - Math.floor(Math.random() * 30 * 24 * 60 * 60 * 1000)).toISOString(),
isActive: true
});
}
// Sample users
state.users = [
{
id: 1,
name: 'Admin User',
email: 'admin@example.com',
avatar: 'https://i.pravatar.cc/150?img=1',
role: 'admin',
isActive: true,
isLocked: false
},
{
id: 2,
name: 'Regular User',
email: 'user@example.com',
avatar: 'https://i.pravatar.cc/150?img=2',
role: 'viewer',
isActive: true,
isLocked: false
},
{
id: 3,
name: 'Family Member',
email: 'family@example.com',
avatar: 'https://i.pravatar.cc/150?img=3',
role: 'viewer',
isActive: true,
isLocked: false
}
];
}
// Check for user session in localStorage
function checkUserSession() {
const user = localStorage.getItem('currentUser');
if (user) {
state.currentUser = JSON.parse(user);
state.isAdmin = state.currentUser.role === 'admin';
updateUserUI();
}
}
// Set up event listeners
function setupEventListeners() {
// Theme toggle
elements.themeToggle.addEventListener('click', toggleDarkMode);
// Infinite scroll for albums
window.addEventListener('scroll', handleAlbumScroll);
// Infinite scroll for photos
window.addEventListener('scroll', handlePhotoScroll);
// Drop zone events
elements.dropZone.addEventListener('click', () => elements.fileInput.click());
elements.fileInput.addEventListener('change', handleFileSelect);
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
elements.dropZone.addEventListener(eventName, preventDefaults, false);
});
['dragenter', 'dragover'].forEach(eventName => {
elements.dropZone.addEventListener(eventName, highlightDropZone, false);
});
['dragleave', 'drop'].forEach(eventName => {
elements.dropZone.addEventListener(eventName, unhighlightDropZone, false);
});
elements.dropZone.addEventListener('drop', handleDrop, false);
// Form submissions
document.getElementById('login-form').addEventListener('submit', handleLogin);
document.getElementById('create-album-form').addEventListener('submit', createAlbum);
document.getElementById('edit-album-form').addEventListener('submit', updateAlbum);
document.getElementById('edit-photo-form').addEventListener('submit', updatePhoto);
document.getElementById('edit-user-form').addEventListener('submit', updateUser);
}
// Prevent default drag and drop behavior
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Highlight drop zone
function highlightDropZone() {
elements.dropZone.classList.add('drag-over');
}
// Unhighlight drop zone
function unhighlightDropZone() {
elements.dropZone.classList.remove('drag-over');
}
// Handle dropped files
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
}
// Handle selected files
function handleFileSelect(e) {
const files = e.target.files;
handleFiles(files);
}
// Process selected files
function handleFiles(files) {
state.uploadedPhotos = [];
if (files.length > 0) {
elements.uploadProgress.classList.remove('hidden');
elements.progressContainer.innerHTML = '';
Array.from(files).forEach((file, index) => {
if (file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(e) {
const photo = {
id: Date.now() + index,
file: file,
url: e.target.result,
title: file.name.replace(/\.[^/.]+$/, ""),
tags: [],
albumId: null
};
state.uploadedPhotos.push(photo);
createProgressBar(photo.id, file.name);
// Simulate upload progress
simulateUploadProgress(photo.id);
// When all files are processed, show edit forms
if (state.uploadedPhotos.length === Array.from(files).filter(f => f.type.startsWith('image/')).length) {
setTimeout(() => {
elements.uploadedPhotosContainer.classList.remove('hidden');
renderPhotoEditForms();
}, 1000);
}
};
reader.readAsDataURL(file);
}
});
}
}
// Create progress bar for upload
function createProgressBar(id, filename) {
const progressBar = document.createElement('div');
progressBar.className = 'space-y-1';
progressBar.id = `progress-${id}`;
progressBar.innerHTML = `
<div class="flex justify-between text-sm">
<span class="truncate max-w-xs">${filename}</span>
<span class="progress-percentage">0%</span>
</div>
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2.5">
<div class="bg-primary-600 h-2.5 rounded-full progress-bar" style="width: 0%"></div>
</div>
`;
elements.progressContainer.appendChild(progressBar);
}
// Simulate upload progress
function simulateUploadProgress(id) {
let progress = 0;
const interval = setInterval(() => {
progress += Math.random() * 10;
if (progress >= 100) {
progress = 100;
clearInterval(interval);
}
updateProgressBar(id, progress);
}, 200);
}
// Update progress bar
function updateProgressBar(id, progress) {
const container = document.getElementById(`progress-${id}`);
if (container) {
const percentage = container.querySelector('.progress-percentage');
const bar = container.querySelector('.progress-bar');
percentage.textContent = `${Math.round(progress)}%`;
bar.style.width = `${progress}%`;
}
}
// Render photo edit forms
function renderPhotoEditForms() {
elements.photoEditForms.innerHTML = '';
state.uploadedPhotos.forEach(photo => {
const form = document.createElement('div');
form.className = 'bg-gray-50 dark:bg-gray-700 p-4 rounded-lg';
form.innerHTML = `
<div class="flex flex-col md:flex-row gap-4">
<div class="md:w-1/3">
<img src="${photo.url}" alt="${photo.title}" class="w-full h-auto rounded-md">
</div>
<div class="md:w-2/3 space-y-3">
<div>
<label class="block text-sm font-medium">Title</label>
<input type="text" value="${photo.title}" onchange="updateUploadedPhoto(${photo.id}, 'title', this.value)" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm font-medium">Tags (comma separated)</label>
<input type="text" value="${photo.tags.join(', ')}" onchange="updateUploadedPhoto(${photo.id}, 'tags', this.value.split(',').map(tag => tag.trim()))" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
</div>
<div>
<label class="block text-sm font-medium">Album</label>
<select onchange="updateUploadedPhoto(${photo.id}, 'albumId', this.value)" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
<option value="">Select an album</option>
${state.albums.map(album => `<option value="${album.id}" ${photo.albumId === album.id ? 'selected' : ''}>${album.title}</option>`).join('')}
</select>
</div>
</div>
</div>
`;
elements.photoEditForms.appendChild(form);
});
}
// Update uploaded photo data
function updateUploadedPhoto(id, field, value) {
const photo = state.uploadedPhotos.find(p => p.id === id);
if (photo) {
photo[field] = value;
}
}
// Save uploaded photos
function saveUploadedPhotos() {
// In a real app, this would upload to a server
// For demo, we'll just add to our photos array
state.uploadedPhotos.forEach(uploadedPhoto => {
const newPhoto = {
id: state.photos.length + 1,
title: uploadedPhoto.title,
url: uploadedPhoto.url,
thumbnailUrl: uploadedPhoto.url, // In real app, generate thumbnail
albumId: parseInt(uploadedPhoto.albumId) || null,
tags: uploadedPhoto.tags,
views: 0,
uploadedAt: new Date().toISOString(),
isActive: true
};
state.photos.push(newPhoto);
// Update album photo count if assigned to an album
if (newPhoto.albumId) {
const album = state.albums.find(a => a.id === newPhoto.albumId);
if (album) {
album.photoCount = (album.photoCount || 0) + 1;
}
}
});
showToast('Photos uploaded successfully!', 'success');
// Reset upload state
state.uploadedPhotos = [];
elements.uploadProgress.classList.add('hidden');
elements.uploadedPhotosContainer.classList.add('hidden');
elements.fileInput.value = '';
// If on photo manager, refresh
if (state.currentPage === 'photo-manager') {
renderPhotoManager();
}
}
// Add URL field for URL uploads
function addUrlField() {
const urlField = document.createElement('div');
urlField.className = 'flex items-center space-x-2';
urlField.innerHTML = `
<input type="url" placeholder="Enter image URL" class="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-gray-700">
<button onclick="removeUrlField(this)" class="p-2 text-red-600 hover:text-red-700">
<i class="fas fa-times"></i>
</button>
`;
elements.urlFields.appendChild(urlField);
}
// Remove URL field
function removeUrlField(button) {
button.parentElement.remove();
}
// Toggle dark mode
function toggleDarkMode() {
if (localStorage.getItem('color-theme') === 'dark' || (!('color-theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.remove('dark');
localStorage.setItem('color-theme', 'light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('color-theme', 'dark');
}
}
// Update user UI based on login state
function updateUserUI() {
if (state.currentUser) {
elements.loginButton.classList.add('hidden');
elements.logoutButton.classList.remove('hidden');
elements.userMenu.classList.remove('hidden');
document.getElementById('user-avatar').src = state.currentUser.avatar || 'https://via.placeholder.com/32';
document.getElementById('username').textContent = state.currentUser.name || 'User';
if (state.isAdmin) {
elements.adminMenuButton.classList.remove('hidden');
} else {
elements.adminMenuButton.classList.add('hidden');
elements.adminMenu.classList.add('hidden');
}
} else {
elements.loginButton.classList.remove('hidden');
elements.logoutButton.classList.add('hidden');
elements.userMenu.classList.add('hidden');
elements.adminMenuButton.classList.add('hidden');
elements.adminMenu.classList.add('hidden');
}
}
// Toggle admin menu
function toggleAdminMenu() {
elements.adminMenu.classList.toggle('hidden');
}
// Show home page
function showHomePage() {
hideAllPages();
elements.homePage.classList.remove('hidden');
state.currentPage = 'home';
document.title = 'Cinematic Gallery - Albums';
}
// Show album page
function showAlbumPage(albumId) {
hideAllPages();
elements.albumPage.classList.remove('hidden');
state.currentPage = 'album';
state.currentAlbum = state.albums.find(a => a.id === albumId);
if (state.currentAlbum) {
document.title = `Cinematic Gallery - ${state.currentAlbum.title}`;
elements.albumTitle.textContent = state.currentAlbum.title;
elements.albumDescription.textContent = state.currentAlbum.description;
// Set visibility badge
elements.albumVisibility.textContent = state.currentAlbum.visibility.charAt(0).toUpperCase() + state.currentAlbum.visibility.slice(1);
elements.albumVisibility.className = 'ml-4 px-3 py-1 text-xs rounded-full ';
if (state.currentAlbum.visibility === 'public') {
elements.albumVisibility.className += 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
} else if (state.currentAlbum.visibility === 'family') {
elements.albumVisibility.className += 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
} else {
elements.albumVisibility.className += 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200';
}
// Clear and load photos
elements.photosMasonry.innerHTML = '';
state.nextPhotoPage = 1;
state.hasMorePhotos = true;
loadPhotos();
}
}
// Show login page
function showLoginPage() {
hideAllPages();
elements.loginPage.classList.remove('hidden');
state.currentPage = 'login';
document.title = 'Cinematic Gallery - Login';
}
// Show upload page
function showUploadPage() {
hideAllPages();
elements.uploadPage.classList.remove('hidden');
state.currentPage = 'upload';
document.title = 'Cinematic Gallery - Upload Photos';
}
// Show photo manager
function showPhotoManager() {
hideAllPages();
elements.photoManager.classList.remove('hidden');
state.currentPage = 'photo-manager';
document.title = 'Cinematic Gallery - Photo Manager';
// Render photo manager
renderPhotoManager();
}
// Show album manager
function showAlbumManager() {
hideAllPages();
elements.albumManager.classList.remove('hidden');
state.currentPage = 'album-manager';
document.title = 'Cinematic Gallery - Album Manager';
// Render album manager
renderAlbumManager();
}
// Show user manager
function showUserManager() {
hideAllPages();
elements.userManager.classList.remove('hidden');
state.currentPage = 'user-manager';
document.title = 'Cinematic Gallery - User Management';
// Render user manager
renderUserManager();
}
// Hide all pages
function hideAllPages() {
elements.homePage.classList.add('hidden');
elements.albumPage.classList.add('hidden');
elements.loginPage.classList.add('hidden');
elements.uploadPage.classList.add('hidden');
elements.photoManager.classList.add('hidden');
elements.albumManager.classList.add('hidden');
elements.userManager.classList.add('hidden');
}
// Load albums
function loadAlbums() {
// In a real app, this would be an API call with pagination
// For demo, we'll simulate loading more albums
// Check if user is logged in to determine which albums to show
const visibleAlbums = state.albums.filter(album => {
if (album.visibility === 'public') return true;
if (!state.currentUser) return false;
if (album.visibility === 'family') return true;
if (album.visibility === 'private') return state.isAdmin;
return false;
});
// Simulate pagination - show 4 albums at a time
const start = (state.nextAlbumPage - 1) * 4;
const end = start + 4;
const albumsToShow = visibleAlbums.slice(start, end);
if (albumsToShow.length === 0) {
state.hasMoreAlbums = false;
return;
}
// Show loading indicator
elements.loadingAlbums.classList.remove('hidden');
// Simulate API delay
setTimeout(() => {
renderAlbums(albumsToShow);
state.nextAlbumPage++;
// Hide loading indicator
elements.loadingAlbums.classList.add('hidden');
// Check if there are more albums to load
state.hasMoreAlbums = end < visibleAlbums.length;
}, 800);
}
// Render albums
function renderAlbums(albums) {
albums.forEach(album => {
const albumCard = document.createElement('div');
albumCard.className = 'album-card bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-all duration-300';
albumCard.innerHTML = `
<div class="relative pb-[75%]">
<img src="${album.coverPhoto}" alt="${album.title}" class="absolute h-full w-full object-cover">
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
<div class="absolute bottom-0 left-0 right-0 p-4">
<h3 class="text-xl font-bold text-white">${album.title}</h3>
<p class="text-sm text-gray-200">${album.photoCount} photos</p>
</div>
</div>
`;
albumCard.addEventListener('click', () => showAlbumPage(album.id));
elements.albumsGrid.appendChild(albumCard);
});
}
// Handle album scroll for infinite loading
function handleAlbumScroll() {
if (state.currentPage !== 'home') return;
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
const isNearBottom = scrollTop + clientHeight >= scrollHeight - 100;
if (isNearBottom && state.hasMoreAlbums && !elements.loadingAlbums.classList.contains('hidden') === false) {
loadAlbums();
}
}
// Load photos for current album
function loadPhotos() {
if (!state.currentAlbum) return;
// In a real app, this would be an API call with pagination
// For demo, we'll simulate loading more photos
// Get photos for current album
const albumPhotos = state.photos.filter(photo => photo.albumId === state.currentAlbum.id);
// Simulate pagination - show 8 photos at a time
const start = (state.nextPhotoPage - 1) * 8;
const end = start + 8;
const photosToShow = albumPhotos.slice(start, end);
if (photosToShow.length === 0) {
state.hasMorePhotos = false;
return;
}
// Show loading indicator
elements.loadingPhotos.classList.remove('hidden');
// Simulate API delay
setTimeout(() => {
renderPhotos(photosToShow);
state.nextPhotoPage++;
// Hide loading indicator
elements.loadingPhotos.classList.add('hidden');
// Check if there are more photos to load
state.hasMorePhotos = end < albumPhotos.length;
}, 800);
}
// Render photos in masonry layout
function renderPhotos(photos) {
photos.forEach(photo => {
const photoItem = document.createElement('div');
photoItem.className = 'masonry-item image-hover';
photoItem.innerHTML = `
<div class="relative group rounded-md overflow-hidden">
<img src="${photo.thumbnailUrl}" alt="${photo.title}" class="w-full h-auto rounded-md">
<div class="absolute inset-0 bg-black/30 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<button class="p-2 bg-white/80 rounded-full hover:bg-white transition">
<i class="fas fa-expand text-gray-800"></i>
</button>
</div>
</div>
<p class="mt-2 text-sm truncate">${photo.title}</p>
`;
// Add click event to show photo in modal
photoItem.querySelector('img').addEventListener('click', () => showPhotoModal(photo));
photoItem.querySelector('button').addEventListener('click', () => showPhotoModal(photo));
elements.photosMasonry.appendChild(photoItem);
});
}
// Handle photo scroll for infinite loading
function handlePhotoScroll() {
if (state.currentPage !== 'album') return;
const { scrollTop, scrollHeight, clientHeight } = document.documentElement;
const isNearBottom = scrollTop + clientHeight >= scrollHeight - 100;
if (isNearBottom && state.hasMorePhotos && !elements.loadingPhotos.classList.contains('hidden') === false) {
loadPhotos();
}
}
// Render photo manager
function renderPhotoManager() {
elements.photosGrid.innerHTML = '';
// Get all photos (in a real app, this would be paginated)
state.photos.forEach(photo => {
const photoCard = document.createElement('div');
photoCard.className = 'bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden';
photoCard.innerHTML = `
<div class="relative pb-[75%]">
<img src="${photo.thumbnailUrl}" alt="${photo.title}" class="absolute h-full w-full object-cover">
<div class="absolute top-
</html>