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,115 @@
<?php
session_start();
if (!isset($_SESSION['user_id']) || $_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(403);
exit(json_encode(['error' => '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;
}
?>