feat: complete photo gallery implementation with album management and lightbox viewer

- Added photo gallery carousel view (gallery.php) with all member albums
- Implemented album detail view with responsive photo grid and lightbox
- Created album creation/editing form with drag-and-drop photo uploads
- Added backend processors for album CRUD operations and photo management
- Implemented API endpoints for fetching and deleting photos
- Added database migration for photo_albums and photos tables
- Included comprehensive feature documentation with testing checklist
- Updated .htaccess with URL rewrite rules for gallery routes
- Added Gallery link to Members Area menu in header
- Created upload directory structure (/assets/uploads/gallery/)
- Implemented security: CSRF tokens, ownership verification, file validation
- Added transaction safety with rollback on errors and cleanup
- Features: Lightbox with keyboard navigation, drag-and-drop uploads, responsive design
This commit is contained in:
twotalesanimation
2025-12-05 09:53:27 +02:00
parent 05f74f1b86
commit 98ef03c7af
12 changed files with 2161 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
<?php
session_start();
if (!isset($_SESSION['user_id']) || $_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(403);
exit('Forbidden');
}
// Validate CSRF token
if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
http_response_code(400);
exit('Invalid request');
}
$rootPath = dirname(dirname(dirname(__DIR__)));
require_once($rootPath . '/connection.php');
require_once($rootPath . '/functions.php');
$conn = openDatabaseConnection();
$title = trim($_POST['title'] ?? '');
$description = trim($_POST['description'] ?? '');
$user_id = $_SESSION['user_id'];
// Validate inputs
if (empty($title) || !validateName($title)) {
$conn->close();
http_response_code(400);
echo json_encode(['error' => 'Album title is required and must be valid']);
exit;
}
if (!empty($description) && strlen($description) > 500) {
$conn->close();
http_response_code(400);
echo json_encode(['error' => 'Description must be 500 characters or less']);
exit;
}
// Create album directory
$album_id = null;
$cover_image = null;
try {
// Start transaction
$conn->begin_transaction();
// Insert album record
$stmt = $conn->prepare("INSERT INTO photo_albums (user_id, title, description, created_at, updated_at) VALUES (?, ?, ?, NOW(), NOW())");
$stmt->bind_param("iss", $user_id, $title, $description);
$stmt->execute();
$album_id = $conn->insert_id;
$stmt->close();
// Create album directory
$albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id;
if (!is_dir($albumDir)) {
if (!mkdir($albumDir, 0755, true)) {
throw new Exception('Failed to create album directory');
}
}
// Handle photo uploads
if (isset($_FILES['photos']) && $_FILES['photos']['error'][0] !== UPLOAD_ERR_NO_FILE) {
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
$maxSize = 5 * 1024 * 1024; // 5MB
$displayOrder = 1;
$firstPhoto = true;
for ($i = 0; $i < count($_FILES['photos']['name']); $i++) {
if ($_FILES['photos']['error'][$i] !== UPLOAD_ERR_OK) {
continue;
}
$fileName = $_FILES['photos']['name'][$i];
$fileTmpName = $_FILES['photos']['tmp_name'][$i];
$fileSize = $_FILES['photos']['size'][$i];
$fileMime = mime_content_type($fileTmpName);
// Validate file
if (!in_array($fileMime, $allowedMimes)) {
throw new Exception('Invalid file type: ' . $fileName);
}
if ($fileSize > $maxSize) {
throw new Exception('File too large: ' . $fileName);
}
// Generate unique filename
$ext = pathinfo($fileName, PATHINFO_EXTENSION);
$newFileName = uniqid('photo_') . '.' . $ext;
$filePath = $albumDir . '/' . $newFileName;
$relativePath = '/assets/uploads/gallery/' . $album_id . '/' . $newFileName;
if (!move_uploaded_file($fileTmpName, $filePath)) {
throw new Exception('Failed to upload: ' . $fileName);
}
// Set first photo as cover
if ($firstPhoto) {
$updateCover = $conn->prepare("UPDATE photo_albums SET cover_image = ? WHERE album_id = ?");
$updateCover->bind_param("si", $relativePath, $album_id);
$updateCover->execute();
$updateCover->close();
$firstPhoto = false;
}
// Insert photo record
$caption = $fileName; // Default caption is filename
$photoStmt = $conn->prepare("INSERT INTO photos (album_id, file_path, caption, display_order, created_at) VALUES (?, ?, ?, ?, NOW())");
$photoStmt->bind_param("issi", $album_id, $relativePath, $caption, $displayOrder);
$photoStmt->execute();
$photoStmt->close();
$displayOrder++;
}
}
// Commit transaction
$conn->commit();
$conn->close();
// Redirect to view album
header('Location: view_album?id=' . $album_id);
exit;
} catch (Exception $e) {
// Rollback on error
$conn->rollback();
$conn->close();
// Clean up partially uploaded files
if ($album_id) {
$albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id;
if (is_dir($albumDir)) {
array_map('unlink', glob($albumDir . '/*'));
rmdir($albumDir);
}
// Delete album record (will cascade delete photos)
$cleanupConn = openDatabaseConnection();
$cleanupConn->query("DELETE FROM photo_albums WHERE album_id = " . intval($album_id));
$cleanupConn->close();
}
http_response_code(400);
echo json_encode(['error' => $e->getMessage()]);
exit;
}
?>