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 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">​</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">​</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">​</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">​</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">​</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> |