From 98ef03c7af6a0ab95da00060a44487548ed03e93 Mon Sep 17 00:00:00 2001 From: twotalesanimation <80506065+twotalesanimation@users.noreply.github.com> Date: Fri, 5 Dec 2025 09:53:27 +0200 Subject: [PATCH] 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 --- .htaccess | 11 + docs/PHOTO_GALLERY_IMPLEMENTATION.md | 494 ++++++++++++++++++ .../003_create_photo_gallery_tables.sql | 30 ++ header.php | 2 + src/pages/gallery/create_album.php | 361 +++++++++++++ src/pages/gallery/gallery.php | 323 ++++++++++++ src/pages/gallery/view_album.php | 366 +++++++++++++ src/processors/delete_album.php | 97 ++++ src/processors/delete_photo.php | 115 ++++ src/processors/get_album_photos.php | 59 +++ src/processors/save_album.php | 150 ++++++ src/processors/update_album.php | 153 ++++++ 12 files changed, 2161 insertions(+) create mode 100644 docs/PHOTO_GALLERY_IMPLEMENTATION.md create mode 100644 docs/migrations/003_create_photo_gallery_tables.sql create mode 100644 src/pages/gallery/create_album.php create mode 100644 src/pages/gallery/gallery.php create mode 100644 src/pages/gallery/view_album.php create mode 100644 src/processors/delete_album.php create mode 100644 src/processors/delete_photo.php create mode 100644 src/processors/get_album_photos.php create mode 100644 src/processors/save_album.php create mode 100644 src/processors/update_album.php diff --git a/.htaccess b/.htaccess index 908abdf0..1f6a7322 100644 --- a/.htaccess +++ b/.htaccess @@ -51,6 +51,12 @@ RewriteRule ^payment_confirmation$ src/pages/shop/payment_confirmation.php [L] RewriteRule ^confirm$ src/pages/shop/confirm.php [L] RewriteRule ^confirm2$ src/pages/shop/confirm2.php [L] +# === GALLERY PAGES === +RewriteRule ^gallery$ src/pages/gallery/gallery.php [L] +RewriteRule ^create_album$ src/pages/gallery/create_album.php [L] +RewriteRule ^edit_album$ src/pages/gallery/create_album.php [L] +RewriteRule ^view_album$ src/pages/gallery/view_album.php [L] + # === EVENTS & BLOG PAGES === RewriteRule ^events$ src/pages/events/events.php [L] RewriteRule ^blog$ src/pages/events/blog.php [L] @@ -121,6 +127,11 @@ RewriteRule ^toggle_trip_published$ src/processors/toggle_trip_published.php [L] RewriteRule ^toggle_event_published$ src/admin/toggle_event_published.php [L] RewriteRule ^delete_trip$ src/processors/delete_trip.php [L] RewriteRule ^delete_event$ src/admin/delete_event.php [L] +RewriteRule ^save_album$ src/processors/save_album.php [L] +RewriteRule ^update_album$ src/processors/update_album.php [L] +RewriteRule ^delete_album$ src/processors/delete_album.php [L] +RewriteRule ^delete_photo$ src/processors/delete_photo.php [L] +RewriteRule ^get_album_photos$ src/processors/get_album_photos.php [L] diff --git a/docs/PHOTO_GALLERY_IMPLEMENTATION.md b/docs/PHOTO_GALLERY_IMPLEMENTATION.md new file mode 100644 index 00000000..69cde658 --- /dev/null +++ b/docs/PHOTO_GALLERY_IMPLEMENTATION.md @@ -0,0 +1,494 @@ +# Photo Gallery Feature - Complete Implementation + +## Overview +The Photo Gallery feature allows 4WDCSA members to create, manage, and view photo albums with a carousel interface for browsing and a lightbox viewer for detailed photo viewing. + +## Database Schema + +### photo_albums table +```sql +- album_id (INT, PK, AUTO_INCREMENT) +- user_id (INT, FK to users) +- title (VARCHAR 255, NOT NULL) +- description (TEXT, nullable) +- cover_image (VARCHAR 500, nullable - stores file path) +- created_at (TIMESTAMP) +- updated_at (TIMESTAMP) +- UNIQUE INDEX on user_id (one album per user for now, can be modified) +- INDEX on created_at for sorting +``` + +### photos table +```sql +- photo_id (INT, PK, AUTO_INCREMENT) +- album_id (INT, FK to photo_albums, CASCADE DELETE) +- file_path (VARCHAR 500, NOT NULL) +- caption (VARCHAR 500, nullable) +- display_order (INT, default 0) +- created_at (TIMESTAMP) +- INDEX on album_id for quick lookups +- INDEX on display_order for sorting +``` + +## File Structure + +### Pages (Public-Facing) +- `src/pages/gallery/gallery.php` - Main carousel view of all albums +- `src/pages/gallery/view_album.php` - Detailed album view with photo grid and lightbox +- `src/pages/gallery/create_album.php` - Form to create new albums and upload initial photos + +### Processors (Backend Logic) +- `src/processors/save_album.php` - Creates new album and handles initial photo uploads +- `src/processors/update_album.php` - Updates album metadata and handles additional photo uploads +- `src/processors/delete_album.php` - Deletes entire album with all photos and files +- `src/processors/delete_photo.php` - Deletes individual photos from album +- `src/processors/get_album_photos.php` - API endpoint returning album photos as JSON + +### Styling +All styling is embedded in each PHP file using ` + +
+
+
+
+
+

+ +
+ + + + + +
+ + +
+ +
+ + +
Optional: Share details about when, where, or why you created this album
+
+ + +
+

Photos in Album

+
+ +
+
+ + +
+ +
+ +
📸
+

Drag and drop photos here or click to select

+
Supports JPG, PNG, GIF, WEBP. Max 5MB per image
+
+
+ +
+ +
+ + Cancel +
+
+ + +
+ +
+ +
+
+
+
+
+ + + + diff --git a/src/pages/gallery/gallery.php b/src/pages/gallery/gallery.php new file mode 100644 index 00000000..78086da4 --- /dev/null +++ b/src/pages/gallery/gallery.php @@ -0,0 +1,323 @@ +query($albums_query); +$albums = []; +if ($result && $result->num_rows > 0) { + while ($row = $result->fetch_assoc()) { + $albums[] = $row; + } +} + +$conn->close(); +?> + + + + 'index.php'], ['Members Area' => '#']]; +require_once($rootPath . '/components/banner.php'); +?> + +
+
+
+
+
+

Member Photo Gallery

+ + Create Album + +
+ + 0): ?> + + +
+ +

No photo albums yet. Be the first to create one!

+ Create Album +
+ +
+
+
+
+ + + + diff --git a/src/pages/gallery/view_album.php b/src/pages/gallery/view_album.php new file mode 100644 index 00000000..314bbd9d --- /dev/null +++ b/src/pages/gallery/view_album.php @@ -0,0 +1,366 @@ +prepare($album_query); +$stmt->bind_param("i", $album_id); +$stmt->execute(); +$album_result = $stmt->get_result(); + +if ($album_result->num_rows === 0) { + $stmt->close(); + $conn->close(); + header('Location: gallery'); + exit; +} + +$album = $album_result->fetch_assoc(); +$stmt->close(); + +// Fetch all photos in the album +$photos_query = " + SELECT photo_id, file_path, caption, display_order + FROM photos + WHERE album_id = ? + ORDER BY display_order ASC +"; + +$stmt = $conn->prepare($photos_query); +$stmt->bind_param("i", $album_id); +$stmt->execute(); +$photos_result = $stmt->get_result(); + +$photos = []; +if ($photos_result && $photos_result->num_rows > 0) { + while ($row = $photos_result->fetch_assoc()) { + $photos[] = $row; + } +} + +$stmt->close(); +$conn->close(); +?> + + + + 'index.php'], ['Gallery' => 'gallery'], [$album['title'] => '#']]; +require_once($rootPath . '/components/banner.php'); +?> + +
+
+
+
+ + Back to Gallery + + +
+
+
+

+ +

+ +

+ + +
+ <?php echo htmlspecialchars($album['first_name']); ?> +
+ + +
+
+
+ + + +
+
+ + 0): ?> + + + + + +
+ +

No photos in this album yet.

+ + Add Photos + +
+ +
+
+
+
+ + + + diff --git a/src/processors/delete_album.php b/src/processors/delete_album.php new file mode 100644 index 00000000..3d2675e9 --- /dev/null +++ b/src/processors/delete_album.php @@ -0,0 +1,97 @@ +prepare("SELECT user_id FROM photo_albums WHERE album_id = ?"); +$albumCheck->bind_param("i", $album_id); +$albumCheck->execute(); +$albumResult = $albumCheck->get_result(); + +if ($albumResult->num_rows === 0) { + $conn->close(); + http_response_code(404); + header('Location: gallery'); + exit; +} + +$album = $albumResult->fetch_assoc(); +if ($album['user_id'] !== $_SESSION['user_id']) { + $conn->close(); + http_response_code(403); + header('Location: gallery'); + exit; +} +$albumCheck->close(); + +try { + // Start transaction + $conn->begin_transaction(); + + // Get all photos for this album + $photoStmt = $conn->prepare("SELECT file_path FROM photos WHERE album_id = ?"); + $photoStmt->bind_param("i", $album_id); + $photoStmt->execute(); + $photoResult = $photoStmt->get_result(); + + // Delete photo files + while ($photo = $photoResult->fetch_assoc()) { + $photoPath = $_SERVER['DOCUMENT_ROOT'] . $photo['file_path']; + if (file_exists($photoPath)) { + unlink($photoPath); + } + } + $photoStmt->close(); + + // Delete photos from database (cascade should handle this) + $deletePhotosStmt = $conn->prepare("DELETE FROM photos WHERE album_id = ?"); + $deletePhotosStmt->bind_param("i", $album_id); + $deletePhotosStmt->execute(); + $deletePhotosStmt->close(); + + // Delete album from database + $deleteAlbumStmt = $conn->prepare("DELETE FROM photo_albums WHERE album_id = ?"); + $deleteAlbumStmt->bind_param("i", $album_id); + $deleteAlbumStmt->execute(); + $deleteAlbumStmt->close(); + + // Delete album directory + $albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id; + if (is_dir($albumDir)) { + rmdir($albumDir); + } + + // Commit transaction + $conn->commit(); + $conn->close(); + + // Redirect to gallery + header('Location: gallery'); + exit; + +} catch (Exception $e) { + // Rollback on error + $conn->rollback(); + $conn->close(); + + http_response_code(400); + echo 'Error deleting album: ' . htmlspecialchars($e->getMessage()); + exit; +} +?> diff --git a/src/processors/delete_photo.php b/src/processors/delete_photo.php new file mode 100644 index 00000000..30494420 --- /dev/null +++ b/src/processors/delete_photo.php @@ -0,0 +1,115 @@ + 'Forbidden'])); +} + +$rootPath = dirname(dirname(dirname(__DIR__))); +require_once($rootPath . '/connection.php'); +require_once($rootPath . '/functions.php'); + +// Validate CSRF token +if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) { + http_response_code(400); + exit(json_encode(['error' => 'Invalid request'])); +} + +$photo_id = intval($_POST['photo_id'] ?? 0); +$user_id = $_SESSION['user_id']; + +if (!$photo_id) { + http_response_code(400); + exit(json_encode(['error' => 'Photo ID is required'])); +} + +$conn = openDatabaseConnection(); + +// Get photo and verify ownership through album +$photoStmt = $conn->prepare(" + SELECT p.photo_id, p.album_id, p.file_path, a.user_id + FROM photos p + JOIN photo_albums a ON p.album_id = a.album_id + WHERE p.photo_id = ? +"); +$photoStmt->bind_param("i", $photo_id); +$photoStmt->execute(); +$photoResult = $photoStmt->get_result(); + +if ($photoResult->num_rows === 0) { + $conn->close(); + http_response_code(404); + exit(json_encode(['error' => 'Photo not found'])); +} + +$photo = $photoResult->fetch_assoc(); +if ($photo['user_id'] !== $user_id) { + $conn->close(); + http_response_code(403); + exit(json_encode(['error' => 'You do not have permission to delete this photo'])); +} +$photoStmt->close(); + +try { + // Delete photo from filesystem + $photoPath = $_SERVER['DOCUMENT_ROOT'] . $photo['file_path']; + if (file_exists($photoPath)) { + unlink($photoPath); + } + + // Delete from database + $deleteStmt = $conn->prepare("DELETE FROM photos WHERE photo_id = ?"); + $deleteStmt->bind_param("i", $photo_id); + $deleteStmt->execute(); + $deleteStmt->close(); + + // Update album's cover image if this was the cover + $albumCheck = $conn->prepare("SELECT cover_image FROM photo_albums WHERE album_id = ?"); + $albumCheck->bind_param("i", $photo['album_id']); + $albumCheck->execute(); + $albumResult = $albumCheck->get_result(); + $album = $albumResult->fetch_assoc(); + $albumCheck->close(); + + if ($album['cover_image'] === $photo['file_path']) { + // Set new cover to first remaining photo + $newCoverStmt = $conn->prepare(" + SELECT file_path FROM photos + WHERE album_id = ? + ORDER BY display_order ASC + LIMIT 1 + "); + $newCoverStmt->bind_param("i", $photo['album_id']); + $newCoverStmt->execute(); + $newCoverResult = $newCoverStmt->get_result(); + + if ($newCoverResult->num_rows > 0) { + $newCover = $newCoverResult->fetch_assoc(); + $updateCoverStmt = $conn->prepare("UPDATE photo_albums SET cover_image = ? WHERE album_id = ?"); + $updateCoverStmt->bind_param("si", $newCover['file_path'], $photo['album_id']); + $updateCoverStmt->execute(); + $updateCoverStmt->close(); + } else { + // No more photos, clear cover image + $clearCoverStmt = $conn->prepare("UPDATE photo_albums SET cover_image = NULL WHERE album_id = ?"); + $clearCoverStmt->bind_param("i", $photo['album_id']); + $clearCoverStmt->execute(); + $clearCoverStmt->close(); + } + $newCoverStmt->close(); + } + + $conn->close(); + + header('Content-Type: application/json'); + echo json_encode(['success' => true]); + exit; + +} catch (Exception $e) { + $conn->close(); + http_response_code(400); + echo json_encode(['error' => $e->getMessage()]); + exit; +} +?> diff --git a/src/processors/get_album_photos.php b/src/processors/get_album_photos.php new file mode 100644 index 00000000..04ea8c55 --- /dev/null +++ b/src/processors/get_album_photos.php @@ -0,0 +1,59 @@ + 'Unauthorized'])); +} + +$rootPath = dirname(dirname(dirname(__DIR__))); +require_once($rootPath . '/connection.php'); + +$album_id = intval($_GET['id'] ?? 0); + +if (!$album_id) { + http_response_code(400); + exit(json_encode(['error' => 'Album ID is required'])); +} + +$conn = openDatabaseConnection(); + +// Verify album exists and user has access +$albumCheck = $conn->prepare("SELECT user_id FROM photo_albums WHERE album_id = ?"); +$albumCheck->bind_param("i", $album_id); +$albumCheck->execute(); +$albumResult = $albumCheck->get_result(); + +if ($albumResult->num_rows === 0) { + $conn->close(); + http_response_code(404); + exit(json_encode(['error' => 'Album not found'])); +} + +$album = $albumResult->fetch_assoc(); +// Allow viewing own albums or public albums (owner is a member) +if ($album['user_id'] !== $_SESSION['user_id']) { + // For now, only allow owners to edit + $conn->close(); + http_response_code(403); + exit(json_encode(['error' => 'Unauthorized'])); +} +$albumCheck->close(); + +// Get photos +$photoStmt = $conn->prepare("SELECT photo_id, file_path, caption, display_order FROM photos WHERE album_id = ? ORDER BY display_order ASC"); +$photoStmt->bind_param("i", $album_id); +$photoStmt->execute(); +$photoResult = $photoStmt->get_result(); + +$photos = []; +while ($photo = $photoResult->fetch_assoc()) { + $photos[] = $photo; +} +$photoStmt->close(); + +$conn->close(); + +header('Content-Type: application/json'); +echo json_encode($photos); +?> diff --git a/src/processors/save_album.php b/src/processors/save_album.php new file mode 100644 index 00000000..dc8aa967 --- /dev/null +++ b/src/processors/save_album.php @@ -0,0 +1,150 @@ +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; +} +?> diff --git a/src/processors/update_album.php b/src/processors/update_album.php new file mode 100644 index 00000000..297d70ab --- /dev/null +++ b/src/processors/update_album.php @@ -0,0 +1,153 @@ +close(); + http_response_code(400); + echo json_encode(['error' => 'Album ID is required']); + exit; +} + +// Verify ownership +$ownerCheck = $conn->prepare("SELECT user_id FROM photo_albums WHERE album_id = ?"); +$ownerCheck->bind_param("i", $album_id); +$ownerCheck->execute(); +$ownerResult = $ownerCheck->get_result(); + +if ($ownerResult->num_rows === 0) { + $conn->close(); + http_response_code(404); + echo json_encode(['error' => 'Album not found']); + exit; +} + +$owner = $ownerResult->fetch_assoc(); +if ($owner['user_id'] !== $user_id) { + $conn->close(); + http_response_code(403); + echo json_encode(['error' => 'You do not have permission to edit this album']); + exit; +} +$ownerCheck->close(); + +// 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; +} + +try { + // Start transaction + $conn->begin_transaction(); + + // Update album + $updateStmt = $conn->prepare("UPDATE photo_albums SET title = ?, description = ?, updated_at = NOW() WHERE album_id = ?"); + $updateStmt->bind_param("ssi", $title, $description, $album_id); + $updateStmt->execute(); + $updateStmt->close(); + + // Handle photo uploads if any + 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 + + $albumDir = $rootPath . '/assets/uploads/gallery/' . $album_id; + + // Get current max display order + $orderStmt = $conn->prepare("SELECT MAX(display_order) as max_order FROM photos WHERE album_id = ?"); + $orderStmt->bind_param("i", $album_id); + $orderStmt->execute(); + $orderResult = $orderStmt->get_result(); + $orderRow = $orderResult->fetch_assoc(); + $displayOrder = ($orderRow['max_order'] ?? 0) + 1; + $orderStmt->close(); + + 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); + } + + // 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 back to album view + header('Location: view_album?id=' . $album_id); + exit; + +} catch (Exception $e) { + // Rollback on error + $conn->rollback(); + $conn->close(); + + http_response_code(400); + echo json_encode(['error' => $e->getMessage()]); + exit; +} +?>