<?php
session_start();
require_once 'config.php';
require_once 'functions.php';
// Проверка, передан ли ID сайта
if (!isset($_GET['id']) || !is_numeric($_GET['id'])) {
redirect('index.php');
}
$site_id = (int)$_GET['id'];
// Обновление статистики просмотров страницы site.php
// Эту функцию вызываем здесь, чтобы считать просмотры именно этой страницы
updateSitePageViews($conn, $site_id);
// Получение информации о сайте, включая site_page_views
$stmt = $conn->prepare("SELECT s.id, s.name, s.url, s.description, s.views, s.hits, s.reputation, s.site_page_views, c.name AS category_name
FROM sites s
JOIN categories c ON s.category_id = c.id
WHERE s.id = ? AND s.approved = 1");
$stmt->bind_param("i", $site_id);
$stmt->execute();
$site = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$site) {
redirect('index.php');
}
// === PHP-код для графиков ===
// Получение статистики счетчика за разные периоды (для текстового отображения)
$stats_day = getCounterStats($conn, $site_id, 'day');
$stats_month = getCounterStats($conn, $site_id, 'month');
$stats_hour = getCounterStats($conn, $site_id, 'hour');
// Получение данных для ежедневного графика за последние 30 дней
$daily_graph_data = getDailyStatsForGraph($conn, $site_id, 30); // Убедитесь, что эта функция возвращает массив
$daily_graph_data_json = json_encode($daily_graph_data);
// Получение данных для почасового графика за последние 24 часа
$hourly_graph_data = getHourlyStatsForGraph($conn, $site_id, 24); // Убедитесь, что эта функция возвращает массив
$hourly_graph_data_json = json_encode($hourly_graph_data);
// ========================================
// Получение общего количества КОРНЕВЫХ отзывов для заголовка вкладки
$stmt = $conn->prepare("SELECT COUNT(*) AS total_reviews FROM reviews WHERE site_id = ? AND parent_id IS NULL");
$stmt->bind_param("i", $site_id);
$stmt->execute();
$total_root_review_count = $stmt->get_result()->fetch_assoc()['total_reviews'];
$stmt->close();
// Получение общего количества ВСЕХ отзывов для заголовка списка внутри вкладки
$stmt = $conn->prepare("SELECT COUNT(*) AS total_reviews FROM reviews WHERE site_id = ?");
$stmt->bind_param("i", $site_id);
$stmt->execute();
$total_review_count = $stmt->get_result()->fetch_assoc()['total_reviews'];
$stmt->close();
// Обработка POST запросов (добавление или редактирование отзыва/ответа/удаления/блокировки)
$review_error = '';
$review_success = '';
$upload_errors_display = ''; // Переменная для отображения ошибок загрузки пользователю
$block_user_message = ''; // Сообщение о результате блокировки/разблокировки
// Проверяем, были ли сообщения переданы через сессию после редиректа
if (isset($_SESSION['upload_errors_display'])) {
$upload_errors_display = $_SESSION['upload_errors_display'];
unset($_SESSION['upload_errors_display']); // Очищаем переменную сессии после использования
}
if (isset($_SESSION['review_success'])) {
$review_success = $_SESSION['review_success'];
unset($_SESSION['review_success']); // Очищаем переменную сессии
}
if (isset($_SESSION['review_error'])) {
$review_error = $_SESSION['review_error'];
unset($_SESSION['review_error']);
}
if (isset($_SESSION['block_user_message'])) {
$block_user_message = $_SESSION['block_user_message'];
unset($_SESSION['block_user_message']);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isLoggedIn()) {
$user_id = $_SESSION['user_id'];
$action = isset($_POST['action']) ? $_POST['action'] : ''; // Получаем тип действия
if ($action === 'add_root_review') {
// Логика добавления нового корневого отзыва
$rating = isset($_POST['rating']) ? (int)$_POST['rating'] : null;
$comment = trim($_POST['comment']);
$parent_id = null; // Корневой отзыв не имеет родителя
// Проверка, выбраны ли файлы для загрузки
$files_selected_for_upload = isset($_FILES['review_images']) && is_array($_FILES['review_images']['name']) && array_filter($_FILES['review_images']['name']);
// Валидация оценки репутации (-5 до +5)
if ($rating === null || $rating < -5 || $rating > 5) {
$review_error = 'Некорректная оценка репутации (должна быть от -5 до +5).';
} elseif (empty($comment) && !$files_selected_for_upload) {
// Комментарий обязателен, только если нет прикрепленных изображений для ЗАГРУЗКИ
$review_error = 'Комментарий отзыва не может быть пустым, если не прикреплено ни одно изображение.';
} else {
// Проверяем, оставлял ли пользователь уже корневой отзыв для этого сайта
$stmt = $conn->prepare("SELECT id FROM reviews WHERE site_id = ? AND user_id = ? AND parent_id IS NULL");
$stmt->bind_param("ii", $site_id, $user_id);
$stmt->execute();
$has_reviewed = $stmt->get_result()->num_rows > 0;
$stmt->close();
if ($has_reviewed) {
$review_error = 'Вы уже оставили основной отзыв на этот сайт.';
} else {
// Добавляем новый корневой отзыв
// Если комментарий пустой, вставляем NULL или пустую строку, если поле в БД не может быть NULL
$comment_to_db = empty($comment) ? null : $comment; // Вставляем NULL, если комментарий пустой (предполагая, что в БД поле TEXT/VARCHAR может быть NULL)
$stmt = $conn->prepare("INSERT INTO reviews (site_id, user_id, parent_id, rating, comment) VALUES (?, ?, ?, ?, ?)");
$stmt->bind_param("iiiss", $site_id, $user_id, $parent_id, $rating, $comment_to_db); // Используем $comment_to_db
if ($stmt->execute()) {
$review_id = $conn->insert_id; // Получаем ID нового отзыва
$current_review_success = 'Отзыв успешно добавлен.'; // Используем временную переменную
// --- Обработка загрузки изображений ---
$upload_dir = 'uploads/reviews/'; // Директория для хранения изображений отзывов (относительный путь)
// Полный путь на сервере. Предполагаем, что uploads находится в той же директории, что и site.php
$upload_base_path = __DIR__ . '/' . $upload_dir;
$upload_errors = []; // Ошибки только для загрузки файлов
// Убедитесь, что директория существует и доступна для записи, только если есть файлы для загрузки
if ($files_selected_for_upload) {
if (!is_dir($upload_base_path)) {
if (!mkdir($upload_base_path, 0775, true)) {
$upload_errors[] = "Не удалось создать директорию для загрузки: " . htmlspecialchars($upload_dir);
}
} elseif (!is_writable($upload_base_path)) {
$upload_errors[] = "Директория для загрузки изображений недоступна для записи: " . htmlspecialchars($upload_dir);
}
}
$allowed_mime_types = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/gif' => 'gif']; // Допустимые MIME-типы и их расширения
$max_file_size = 5 * 1024 * 1024; // УВЕЛИЧЕН РАЗМЕР ДО 5MB
$max_files = 5;
$uploaded_count = 0;
$uploaded_paths_db = []; // Пути для сохранения в БД (относительные)
// Проверяем, были ли файлы выбраны и директория доступна
if ($files_selected_for_upload && empty($upload_errors)) {
// Перебираем каждый загруженный файл
foreach ($_FILES['review_images']['name'] as $key => $name) {
// Проверяем, был ли файл успешно загружен без ошибок PHP на сервер во временную папку
if ($_FILES['review_images']['error'][$key] === UPLOAD_ERR_OK) {
$tmp_name = $_FILES['review_images']['tmp_name'][$key];
$file_size = $_FILES['review_images']['size'][$key];
$original_name = escape($name); // Экранируем имя файла для сообщений
// Получаем MIME-тип файла более надежным способом
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$file_type = finfo_file($finfo, $tmp_name);
finfo_close($finfo);
// Проверка на количество файлов
if ($uploaded_count >= $max_files) {
$upload_errors[] = "Файл '{$original_name}' пропущен: Превышено максимальное количество ({$max_files}).";
continue; // Пропускаем этот файл
}
// Валидация MIME-типа
if (!array_key_exists($file_type, $allowed_mime_types)) {
$upload_errors[] = "Файл '{$original_name}' пропущен: Недопустимый тип файла ({$file_type}). Разрешены: JPG, PNG, GIF.";
continue; // Пропускаем этот файл
}
// Валидация размера файла
if ($file_size > $max_file_size) {
$upload_errors[] = "Файл '{$original_name}' пропущен: Превышает допустимый размер ({".($max_file_size/1024/1024)."MB).";
continue; // Пропускаем этот файл
}
// Получаем расширение из разрешенных MIME-типов
$file_ext = $allowed_mime_types[$file_type];
// Генерируем уникальное имя файла и полный путь сохранения
$new_file_name = uniqid('review_img_') . '.' . $file_ext;
$destination_path = $upload_base_path . $new_file_name; // Полный путь на сервере
// Перемещаем загруженный файл из временной директории в целевую
if (move_uploaded_file($tmp_name, $destination_path)) {
$uploaded_paths_db[] = $upload_dir . $new_file_name; // Сохраняем ОТНОСИТЕЛЬНЫЙ путь для записи в БД
$uploaded_count++;
} else {
$upload_errors[] = "Файл '{$original_name}' пропущен: Ошибка при перемещении на сервер.";
}
} elseif ($_FILES['review_images']['error'][$key] !== UPLOAD_ERR_NO_FILE) {
// Обрабатываем другие возможные ошибки загрузки PHP (кроме случая, когда файл не был выбран)
$error_code = $_FILES['review_images']['error'][$key];
$error_message = "Файл '{$original_name}' пропущен: Ошибка загрузки PHP (код {$error_code}).";
// Можно добавить более детальные сообщения для кодов ошибок, если нужно
$upload_errors[] = $error_message;
}
// UPLOAD_ERR_NO_FILE (код 4) игнорируем, так как это нормально, если не все поля input[type=file] были использованы.
} // Конец цикла по файлам
// Вставляем ОТНОСИТЕЛЬНЫЕ пути к загруженным изображениям в базу данных
if (!empty($uploaded_paths_db)) {
$sql = "INSERT INTO review_images (review_id, image_path) VALUES (?, ?)";
$stmt_img = $conn->prepare($sql);
if ($stmt_img) {
foreach ($uploaded_paths_db as $path_db) {
$stmt_img->bind_param("is", $review_id, $path_db); // Используем подготовленное выражени
$stmt_img->execute();
}
$stmt_img->close();
} else {
error_log("Failed to prepare statement for inserting review_images: " . $conn->error);
$upload_errors[] = "Внутренняя ошибка сервера при сохранении изображений.";
}
}
} // Конец if($files_selected_for_upload && empty($upload_errors))
// Формируем сообщение об ошибках загрузки для вывода пользователю
if (!empty($upload_errors)) {
$upload_errors_display = '<div class="alert alert-error">Ошибки при загрузке изображений:<br>' . implode('<br>', $upload_errors) . '</div>';
}
// --- Конец обработки загрузки изображений ---
// Обновляем репутацию сайта после добавления корневого отзыва
updateSiteReputation($conn, $site_id);
// Передаем сообщения через сессию и перенаправляем
$_SESSION['upload_errors_display'] = $upload_errors_display;
$_SESSION['review_success'] = $current_review_success; // Передаем успех отзыва
redirect("site.php?id=$site_id&tab=reviews-list#review-" . $review_id); // Перенаправляем на добавленный отзыв
} else {
// Если не удалось добавить сам отзыв
$_SESSION['review_error'] = 'Ошибка добавления отзыва в базу данных.';
redirect("site.php?id=$site_id&tab=reviews-list"); // Перенаправляем обратно с ошибкой
}
}
}
} elseif ($action === 'add_reply' && isset($_POST['parent_id']) && is_numeric($_POST['parent_id'])) {
// Логика добавления ответа на отзыв
$parent_id = (int)$_POST['parent_id'];
$comment = trim($_POST['comment']);
// Ответы не имеют оценки репутации, вставляем 0 (т.к. в БД NOT NULL)
$reply_rating = 0;
// ИСПРАВЛЕНО: Проверка статуса блокировки пользователя перед добавлением ответа
if (isUserBlocked($conn, $user_id)) {
$_SESSION['review_error'] = 'Вы заблокированы и не можете оставлять ответы.';
} elseif (empty($comment)) { // Комментарий для ответа обязателен
$_SESSION['review_error'] = 'Комментарий ответа не может быть пустым.';
} else {
$stmt = $conn->prepare("INSERT INTO reviews (site_id, user_id, parent_id, rating, comment) VALUES (?, ?, ?, ?, ?)");
$stmt->bind_param("iiiss", $site_id, $user_id, $parent_id, $reply_rating, $comment);
if ($stmt->execute()) {
$_SESSION['review_success'] = 'Ответ успешно добавлен.';
$stmt->close();
// Ответы не меняют репутацию сайта напрямую
redirect("site.php?id=$site_id&tab=reviews-list#review-" . $parent_id); // Перенаправляем на родительский отзыв
} else {
$_SESSION['review_error'] = 'Ошибка добавления ответа.';
$stmt->close();
}
}
redirect("site.php?id=$site_id&tab=reviews-list"); // Перенаправляем в любом случае
} elseif ($action === 'edit_review' && isset($_POST['review_id']) && is_numeric($_POST['review_id'])) {
// Логика редактирования существующего корневого отзыва
$review_id = (int)$_POST['review_id'];
$rating = isset($_POST['rating']) ? (int)$_POST['rating'] : null;
$comment = trim($_POST['comment']);
// Получаем отзыв для проверки владения и того, что это корневой отзыв
$stmt = $conn->prepare("SELECT id, user_id, site_id, parent_id FROM reviews WHERE id = ?");
$stmt->bind_param("i", $review_id);
$stmt->execute();
$existing_review = $stmt->get_result()->fetch_assoc();
$stmt->close();
if (!$existing_review || (int)$existing_review['user_id'] !== (int)$user_id || $existing_review['parent_id'] !== null) {
// Отзыв не найден, или пользователь не автор, или это не корневой отзыв
$_SESSION['review_error'] = 'Невозможно отредактировать этот отзыв.';
} elseif ($rating === null || $rating < -5 || $rating > 5) {
$_SESSION['review_error'] = 'Некорректная оценка репутации (должна быть от -5 до +5).';
} elseif (empty($comment)) { // Комментарий не может стать пустым при редактировании, если нет изображений? Редактирование текста не связано с изображениями, комментарий обязателен для корневого отзыва.
$_SESSION['review_error'] = 'Комментарий не может быть пустым.';
} else {
// --- Обработка удаления изображений при редактировании ---
if (isset($_POST['delete_images']) && is_array($_POST['delete_images'])) {
$upload_dir = 'uploads/reviews/'; // Относительный путь
// Полный путь на сервере для удаления. Предполагаем, что uploads в той же директории.
$upload_base_path = __DIR__ . '/' . $upload_dir;
foreach ($_POST['delete_images'] as $image_id_to_delete) {
$image_id_to_delete = (int)$image_id_to_delete;
if ($image_id_to_delete > 0) {
// Получаем информацию об изображении, чтобы убедиться, что оно принадлежит редактируемому отзыву
$stmt_check_img = $conn->prepare("SELECT id, review_id, image_path FROM review_images WHERE id = ?");
if ($stmt_check_img) {
$stmt_check_img->bind_param("i", $image_id_to_delete);
$stmt_check_img->execute();
$img_to_delete = $stmt_check_img->get_result()->fetch_assoc();
$stmt_check_img->close();
// Проверяем, что изображение найдено и относится к текущему отзыву
if ($img_to_delete && (int)$img_to_delete['review_id'] === $review_id) {
// Удаляем запись из БД
$stmt_del_img = $conn->prepare("DELETE FROM review_images WHERE id = ?");
if ($stmt_del_img) {
$stmt_del_img->bind_param("i", $image_id_to_delete);
if ($stmt_del_img->execute()) {
// Удаляем физический файл с сервера
// Используем полный путь на сервере, построенный корректно
$physical_path = $upload_base_path . basename($img_to_delete['image_path']); // Используем basename для безопасности
if (file_exists($physical_path)) {
unlink($physical_path);
} else {
error_log("Failed to delete physical file (not found): " . $physical_path);
}
} else {
error_log("Failed to delete review_image from DB (ID: {$image_id_to_delete}): " . $conn->error);
// Можно добавить пользователю сообщение об ошибке удаления изображения, если нужно
}
$stmt_del_img->close();
}
}
}
}
}
}
// --- Конец обработки удаления изображений ---
// Обновляем сам отзыв (текст и рейтинг)
$stmt = $conn->prepare("UPDATE reviews SET rating = ?, comment = ? WHERE id = ?");
$stmt->bind_param("isi", $rating, $comment, $review_id);
if ($stmt->execute()) {
$_SESSION['review_success'] = 'Отзыв успешно обновлен.';
$stmt->close();
// Обновляем репутацию сайта после редактирования корневого отзыва
updateSiteReputation($conn, (int)$existing_review['site_id']); // Пересчитываем и обновляем, используем site_id из fetched review
redirect("site.php?id=" . (int)$existing_review['site_id'] . "&tab=reviews-list#review-" . $review_id); // Перенаправляем на отредактированный отзыв
} else {
$_SESSION['review_error'] = 'Ошибка обновления отзыва.';
$stmt->close();
redirect("site.php?id=" . (int)$existing_review['site_id'] . "&tab=reviews-list#review-" . $review_id); // Перенаправляем с ошибкой
}
}
redirect("site.php?id=" . (int)$existing_review['site_id'] . "&tab=reviews-list#review-" . $review_id); // Перенаправляем в любом случае при ошибке валидации
} elseif ($action === 'delete_review' && isset($_POST['review_id']) && is_numeric($_POST['review_id']) && isLoggedIn()) {
// Логика удаления отзыва или ответа
$review_id = (int)$_POST['review_id'];
$user_id = $_SESSION['user_id']; // Logged-in user
// Получаем отзыв для проверки владения или админ статуса, а также site_id для перенаправления и репутации
$stmt = $conn->prepare("SELECT id, user_id, site_id, parent_id FROM reviews WHERE id = ?");
$stmt->bind_param("i", $review_id);
$stmt->execute();
$review_to_delete = $stmt->get_result()->fetch_assoc();
$stmt->close();
if ($review_to_delete) {
// Проверяем, является ли авторизованный пользователь автором ИЛИ администратором
if ((int)$review_to_delete['user_id'] === (int)$user_id || isAdmin()) {
// Пользователь имеет права на удаление
// --- Удаление физических файлов изображений перед удалением отзыва из БД ---
// Получаем пути к изображениям, связанным с этим отзывом
$image_paths_to_delete = [];
$stmt_img_paths = $conn->prepare("SELECT image_path FROM review_images WHERE review_id = ?");
if ($stmt_img_paths) {
$stmt_img_paths->bind_param("i", $review_id);
$stmt_img_paths->execute();
$result_img_paths = $stmt_img_paths->get_result();
while ($row_img_paths = $result_img_paths->fetch_assoc()) {
$image_paths_to_delete[] = $row_img_paths['image_path'];
}
$stmt_img_paths->close();
}
// Удаляем сам отзыв (это вызовет каскадное удаление записей изображений и ответов в БД)
$stmt = $conn->prepare("DELETE FROM reviews WHERE id = ?");
$stmt->bind_param("i", $review_id);
if ($stmt->execute()) {
$_SESSION['review_success'] = 'Отзыв/ответ успешно удален.';
$stmt->close();
// Удаляем физические файлы изображений после успешного удаления из БД
$upload_dir = 'uploads/reviews/'; // Относительный путь
// Полный путь на сервере для удаления. Предполагаем, что uploads в той же директории.
$upload_base_path = __DIR__ . '/' . $upload_dir;
foreach ($image_paths_to_delete as $path) {
// Используем полный путь на сервере, построенный корректно
$physical_path = $upload_base_path . basename($path); // Используем basename для безопасности
if (file_exists($physical_path)) { // Проверяем существование файла перед удалением
unlink($physical_path); // Удаляем файл
} else {
error_log("Failed to delete physical file (not found): " . $physical_path);
}
}
// Если удален корневой отзыв, пересчитываем репутацию сайта
// parent_id берется из review_to_delete, который мы получили ДО удаления
if ($review_to_delete['parent_id'] === null) {
updateSiteReputation($conn, (int)$review_to_delete['site_id']); // Пересчитываем и обновляем репутацию
}
// Перенаправляем обратно на вкладку отзывов
redirect("site.php?id=" . (int)$review_to_delete['site_id'] . "&tab=reviews-list");
} else {
$_SESSION['review_error'] = 'Ошибка удаления отзыва/ответа из базы данных.';
redirect("site.php?id=" . (int)$review_to_delete['site_id'] . "&tab=reviews-list");
}
} else {
// Пользователь не имеет прав
$_SESSION['review_error'] = 'У вас нет прав для удаления этого отзыва/ответа.';
redirect("site.php?id=" . (int)$review_to_delete['site_id'] . "&tab=reviews-list");
}
} else {
// Отзыв/ответ не найден
$_SESSION['review_error'] = 'Отзыв/ответ не найден.';
// Если site_id не известен (отзыв не найден), редиректим на главную или список сайтов
redirect("index.php"); // Перенаправляем на главную
}
} elseif ($action === 'toggle_block_user' && isset($_POST['user_to_block_id']) && is_numeric($_POST['user_to_block_id']) && isAdmin()) {
// Новая логика для блокировки/разблокировки пользователя
$user_to_block_id = (int)$_POST['user_to_block_id'];
$current_block_status = isUserBlocked($conn, $user_to_block_id); // Получаем текущий статус
// Переключаем статус
$new_block_status = !$current_block_status;
if (toggleUserBlockStatus($conn, $user_to_block_id, $new_block_status)) {
$_SESSION['block_user_message'] = 'Статус блокировки пользователя успешно обновлен.';
} else {
$_SESSION['block_user_message'] = 'Ошибка при обновлении статуса блокировки пользователя.';
}
// Перенаправляем обратно на страницу сайта, откуда пришел запрос
redirect("site.php?id=$site_id&tab=reviews-list");
}
// else: Обработка других возможных POST действий
// Если ни одно из действий не совпало, или возникла ошибка до редиректа
// Убедимся, что сессионные переменные установлены, если были ошибки
if (!empty($review_error) && !isset($_SESSION['review_error'])) {
$_SESSION['review_error'] = $review_error;
}
if (!empty($review_success) && !isset($_SESSION['review_success'])) {
$_SESSION['review_success'] = $review_success;
}
if (!empty($upload_errors_display) && !isset($_SESSION['upload_errors_display'])) {
$_SESSION['upload_errors_display'] = $upload_errors_display;
}
if (!empty($block_user_message) && !isset($_SESSION['block_user_message'])) {
$_SESSION['block_user_message'] = $block_user_message;
}
// Уже есть редиректы внутри каждого успешного/неуспешного действия.
// Этот блок ниже, возможно, избыточен, но оставим на всякий случай, если что-то пойдет не по плану.
// redirect("site.php?id=$site_id&tab=reviews-list"); // Избегать двойных редиректов
}
// --- Получение и очистка сессионных сообщений для отображения после загрузки страницы ---
// Этот блок уже находится выше, перед обработкой POST.
// Переместим его ниже, чтобы он был после определения всех возможных сообщений.
// Но лучше оставить его там, где он сейчас, чтобы сообщения были доступны сразу после загрузки страницы.
// OK, оставляем блок получения сессионных сообщений в начале PHP части.
// Получение отзывов (древовидная структура)
$reviews = [];
$all_reviews = [];
// Сортировка по убыванию даты для новых отзывов первыми
$result = $conn->query("SELECT r.id, r.rating, r.comment, r.created_at, r.parent_id, u.login, r.user_id
FROM reviews r
JOIN users u ON r.user_id = u.id
WHERE r.site_id = $site_id
ORDER BY r.created_at DESC");
// Убедитесь, что запрос выполнен успешно перед fetch_assoc
if ($result === FALSE) {
error_log("Error fetching reviews: " . $conn->error);
// Можно установить $reviews в пустой массив или вывести ошибку пользователю
$all_reviews = []; // Убеждаемся, что массив пуст, если запрос провалился
} else {
while ($row = $result->fetch_assoc()) {
$all_reviews[$row['id']] = $row;
}
$result->free(); // Освобождаем результат запроса
}
// ======================================================================
// Передаем $conn в функцию buildReviewTree для получения изображений
function buildReviewTree(&$elements, $parentId = null, $conn) {
$branch = array();
// Сортируем элементы по parent_id для правильной сборки дерева
$sorted_elements = [];
foreach ($elements as $element) {
$sorted_elements[$element['parent_id']][] = $element;
}
// Если у текущего parentId есть дети
if (isset($sorted_elements[$parentId])) {
// Дополнительная сортировка по created_at в рамках одного parent_id
usort($sorted_elements[$parentId], function($a, $b) {
// Корневые отзывы уже отсортированы в главном запросе DESC.
// Ответы внутри ветки сортируем ASC.
// Проверяем, является ли родительский ID корневым (null). Если да, используем исходный порядок.
// Если parentId не null (это ответ), сортируем дочерние элементы (ответы) по возрастанию даты.
if ($parentId === null) {
// Если сортируем корневые элементы (parentId === null), используем исходный порядок (DESC из SQL)
// Просто возвращаем 0, чтобы не менять порядок, установленный SQL.
// Фактически этот usort для корневых может быть не нужен, т.к. они уже отсортированы.
return 0; // Сохраняем порядок, установленный SQL
} else {
// Если сортируем ответы (parentId !== null), сортируем по возрастанию даты
return strtotime($a['created_at']) - strtotime($b['created_at']);
}
});
foreach ($sorted_elements[$parentId] as $element) {
// === Получение изображений для текущего отзыва ===
$images = [];
if ($conn) { // Ensure connection is valid
$stmt_img = $conn->prepare("SELECT id, image_path FROM review_images WHERE review_id = ?");
if ($stmt_img) {
$stmt_img->bind_param("i", $element['id']);
$stmt_img->execute();
$result_img = $stmt_img->get_result();
while ($row_img = $result_img->fetch_assoc()) {
$images[] = $row_img;
}
$stmt_img->close();
// Добавляем изображения к элементу отзыва
$element['images'] = $images;
} else {
error_log("Failed to prepare statement for review_images in buildReviewTree: " . $conn->error);
}
}
// ================================================
$children = buildReviewTree($elements, $element['id'], $conn); // Recursive call
if ($children) {
// Вложенные ответы уже отсортированы рекурсивным вызовом с условием parentId !== null
$element['replies'] = $children;
} else {
$element['replies'] = [];
}
$branch[$element['id']] = $element;
}
}
return $branch;
}
// Передаем $conn в функцию buildReviewTree
$reviews = buildReviewTree($all_reviews, null, $conn);
// Получение категорий для сайдбара - оставляем без изменений
$categories = [];
$result = $conn->query("SELECT id, name FROM categories ORDER BY name");
while ($row = $result->fetch_assoc()) {
$categories[] = $row;
}
// Путь к скриншоту - теперь используем SITE_URL, который должен быть HTTPS
$screenshot_path = SITE_URL . '/screenshots/' . $site['id'] . '.png';
// Полный путь на сервере для проверки файла. Предполагаем, что screenshots в той же директории, что и site.php
$local_screenshot_path = __DIR__ . '/screenshots/' . $site['id'] . '.png';
if (!file_exists($local_screenshot_path) || filesize($local_screenshot_path) === 0) {
$screenshot_path = SITE_URL . '/screenshots/placeholder.png'; // Используем placeholder через SITE_URL (HTTPS)
}
?>
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo escape($site['name']); ?> - Toppyc.ru</title>
<link rel="stylesheet" href="styles.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
</head>
<body>
<div class="container">
<?php include 'sidebar_left.php'; ?>
<div class="main-content">
<h2><?php echo escape($site['name']); ?></h2>
<div class="site-card">
<div class="site-card-image-container">
<img src="<?php echo escape($screenshot_path); ?>" alt="Скриншот сайта <?php echo escape($site['name']); ?>">
</div>
<div class="site-info">
<h3><?php echo escape($site['name']); ?></h3>
<p><strong>URL:</strong> <a href="<?php echo escape($site['url']); ?>" target="_blank"><?php echo escape($site['url']); ?></a></p>
<p><strong>Описание:</strong> <?php echo escape($site['description']); ?></p>
<p><strong>Категория:</strong> <?php echo escape($site['category_name']); ?></p>
<p><strong>Репутация:</strong> <span class="reputation <?php echo $site['reputation'] < 0 ? 'negative' : ($site['reputation'] > 0 ? 'positive' : ''); ?>"><?php echo $site['reputation']; ?></span></p>
</div>
</div>
<div class="tab-container">
<div class="tab active" data-tab="stats"><i class="fas fa-chart-line"></i> Статистика</div>
<div class="tab" data-tab="graph"><i class="fas fa-chart-area"></i> График</div>
<div class="tab" data-tab="reviews-list"><i class="fas fa-list-alt"></i> Отзывы (<span id="review-count"><?php echo $total_root_review_count; ?></span>)</div>
</div>
<div id="stats-content" class="tab-content active">
<h3>Статистика счетчика</h3>
<p>Статистика, которая собирается с установленного счетчика на вашем сайте.</p>
<div class="stats-block-container">
<div class="stats-block-period">
<h4>За последний час</h4>
<p class="stats-item"><i class="fas fa-users"></i> Уникальных посетителей: <?php echo $stats_hour['unique_visitors']; ?></p>
<p class="stats-item"><i class="fas fa-mouse-pointer"></i> Просмотров: <?php echo $stats_hour['hits']; ?></p>
</div>
<div class="stats-block-period">
<h4>За сутки</h4>
<p class="stats-item"><i class="fas fa-users"></i> Уникальных посетителей: <?php echo $stats_day['unique_visitors']; ?></p>
<p class="stats-item"><i class="fas fa-eye"></i> Просмотров: <?php echo $stats_day['views']; ?></p>
</div>
<div class="stats-block-period">
<h4>За месяц</h4>
<p class="stats-item"><i class="fas fa-users-cog"></i> Уникальных посетителей: <?php echo $stats_month['unique_visitors']; ?></p>
<p class="stats-item"><i class="fas fa-chart-bar"></i> Просмотров: <?php echo $stats_month['views']; ?></p>
</div>
</div>
</div>
<div id="graph-content" class="tab-content">
<h3>Статистика за 30 дней (ежедневно)</h3>
<div class="chart-container" style="position: relative; height:300px; width:100%; margin-bottom: 40px;">
<canvas id="dailyStatsChart"></canvas>
</div>
<h3>Статистика за последние 24 часа (по часам)</h3>
<?php if (empty($hourly_graph_data)): ?>
<p>Недостаточно данных для построения почасового графика за последние 24 часа.</p>
<?php else: ?>
<div class="chart-container" style="position: relative; height:300px; width:100%;">
<canvas id="hourlyStatsChart"></canvas>
</div>
<?php endif; ?>
</div>
<div id="reviews-list-content" class="tab-content">
<?php if (isLoggedIn()): ?>
<?php
// Проверяем, оставлял ли текущий пользователь уже корневой отзыв для этого сайта
$user_id = $_SESSION['user_id'];
$stmt = $conn->prepare("SELECT id FROM reviews WHERE site_id = ? AND user_id = ? AND parent_id IS NULL");
$stmt->bind_param("ii", $site_id, $user_id);
$stmt->execute();
$has_reviewed = $stmt->get_result()->num_rows > 0;
$stmt->close();
if (!$has_reviewed): // Если пользователь еще не оставил основной отзыв
?>
<div class="review-form-container">
<h3>Оставить отзыв</h3>
<?php if (!empty($review_success)): ?>
<div class="alert alert-success"><?php echo escape($review_success); ?></div>
<?php endif; ?>
<?php if (!empty($review_error)): ?>
<div class="alert alert-error"><?php echo escape($review_error); ?></div>
<?php endif; ?>
<?php echo $upload_errors_display; ?>
<form action="site.php?id=<?php echo $site['id']; ?>" method="POST" id="add-review-form" enctype="multipart/form-data">
<input type="hidden" name="action" value="add_root_review"> <p>
<label for="rating">Репутация:</label>
<select id="rating" name="rating" class="input" required>
<?php for ($i = 5; $i >= -5; $i--): ?>
<option value="<?php echo $i; ?>"><?php echo $i > 0 ? '+' : ''; echo $i; ?></option>
<?php endfor; ?>
</select>
</p>
<p>
<label for="comment">Ваш отзыв (до 1000 символов):</label>
<textarea id="comment" name="comment" class="input" maxlength="1000"></textarea> </p>
<p>
<label for="review_images">Прикрепить изображения (до 5 файлов, JPG, PNG, GIF, до 5MB каждый):</label>
<input type="file" id="review_images" name="review_images[]" class="input" accept="image/jpeg, image/png, image/gif" multiple>
<small>Максимум 5 файлов, каждый не более 5MB.</small>
</p>
<div id="selected-files-container" style="margin-top: 5px; font-size: 0.9em;"></div>
<button type="submit" class="btn btn-primary" id="submit-review-button" disabled><i class="fas fa-paper-plane"></i> Оставить отзыв</button>
</form>
</div>
<?php else: // Если пользователь уже оставил основной отзыв ?><p>Вы уже оставили основной отзыв на этот сайт.</p><?php endif; ?>
<?php else: // Если пользователь не авторизован ?>
<p>Пожалуйста, <a href="login.php">войдите</a>, чтобы оставить отзыв.</p>
<?php endif; ?>
<?php
// Удален заголовок h3 "Отзывы пользователей" и лишние символы
?>
<?php if (!empty($review_success) && $_SERVER['REQUEST_METHOD'] !== 'POST'): ?>
<div class="alert alert-success"><?php echo escape($review_success); ?></div>
<?php endif; ?>
<?php if (!empty($review_error) && $_SERVER['REQUEST_METHOD'] !== 'POST'): ?>
<div class="alert alert-error"><?php echo escape($review_error); ?></div>
<?php endif; ?>
<?php if (!empty($block_user_message)): ?>
<div class="alert <?php echo strpos($block_user_message, 'успешно') !== false ? 'alert-success' : 'alert-error'; ?>"><?php echo escape($block_user_message); ?></div>
<?php endif; ?>
<?php if (empty($reviews)): ?>
<p>Отзывов пока нет.</p>
<?php else: ?>
<?php
// Функция для рекурсивного вывода отзывов и ответов
function displayReviews($reviews, $site_id, $loggedInUserId, $conn) {
foreach ($reviews as $review) {
// Проверяем статус блокировки автора ответа
$is_author_blocked = isUserBlocked($conn, $review['user_id']);
?>
<div class="review" id="review-<?php echo $review['id']; ?>">
<div class="review-display">
<p>
<strong><?php echo escape($review['login']); ?></strong>
<?php if ($review['parent_id'] === null): // Показываем оценку только для корневых отзывов ?>
<span class="reputation <?php echo $review['rating'] < 0 ? 'negative' : ($review['rating'] > 0 ? 'positive' : ''); // ИСПРАВЛЕНО: Добавлен класс positive ?>">
<?php echo $review['rating'] > 0 ? '+' : ''; ?>
<?php echo escape($review['rating']); ?>
</span>
<?php endif; ?>
<span class="review-date">(<?php echo $review['created_at']; ?>)</span>
<?php if (isAdmin() && $review['parent_id'] !== null): // Кнопка "Заблокировать" / "Разблокировать" только для АДМИНА и только для ОТВЕТОВ ?>
<?php
$is_author_of_reply_blocked = isUserBlocked($conn, $review['user_id']);
$button_text = $is_author_of_reply_blocked ? 'Разблокировать' : 'Заблокировать';
$button_class = $is_author_of_reply_blocked ? 'btn-success' : 'btn-danger';
$new_status_value = $is_author_of_reply_blocked ? 0 : 1;
$icon_class = $is_author_of_reply_blocked ? 'fas fa-unlock' : 'fas fa-user-slash';
?>
<form action="site.php?id=<?php echo $site_id; ?>&tab=reviews-list" method="POST" style="display: inline-block; margin-left: 10px;">
<input type="hidden" name="action" value="toggle_block_user">
<input type="hidden" name="user_to_block_id" value="<?php echo $review['user_id']; ?>">
<input type="hidden" name="block_status" value="<?php echo $new_status_value; ?>">
<button type="submit" class="btn btn-small <?php echo $button_class; ?>"><i class="<?php echo $icon_class; ?>"></i> <?php echo $button_text; ?></button>
</form>
<?php endif; ?>
</p>
<div class="review-comment"><?php echo nl2br(escape($review['comment'])); ?></div>
<?php
$images = isset($review['images']) ? $review['images'] : [];
?>
<?php if (!empty($images)): ?>
<div class="review-images-container">
<?php foreach ($images as $image): ?>
<?php
$image_path_clean = ltrim($image['image_path'], '/');
$image_url = SITE_URL . '/' . escape($image_path_clean);
?>
<img src="<?php echo $image_url; ?>" alt="Изображение к отзыву" class="review-thumbnail" data-full-img-url="<?php echo $image_url; ?>">
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php if ($loggedInUserId !== null): ?>
<?php
$can_delete = false;
if ((int)$review['user_id'] === (int)$loggedInUserId || isAdmin()) {
$can_delete = true;
}
?>
<?php if ($can_delete): ?>
<form action="site.php?id=<?php echo $site_id; ?>&tab=reviews-list" method="POST" class="delete-review-form" style="display: inline-block; margin-right: 5px;">
<input type="hidden" name="action" value="delete_review">
<input type="hidden" name="review_id" value="<?php echo $review['id']; ?>">
<button type="submit" class="btn btn-danger btn-small delete-button" onclick="return confirm('Вы уверены, что хотите удалить этот отзыв/ответ?');"><i class="fas fa-trash"></i> Удалить</button>
</form>
<?php endif; ?>
<?php if ((int)$review['user_id'] === (int)$loggedInUserId && $review['parent_id'] === null): ?>
<button class="btn btn-secondary btn-small edit-review-button" data-review-id="<?php echo $review['id']; ?>"><i class="fas fa-edit"></i> Редактировать</button>
<?php endif; ?>
<?php if (!isUserBlocked($conn, $loggedInUserId)): ?>
<button class="btn btn-secondary btn-small reply-button" data-review-id="<?php echo $review['id']; ?>"><i class="fas fa-reply"></i> Ответить</button>
<?php endif; ?>
<form action="site.php?id=<?php echo $site_id; ?>" method="POST" class="reply-form" id="reply-form-<?php echo $review['id']; ?>" style="display: none;">
<input type="hidden" name="action" value="add_reply">
<input type="hidden" name="parent_id" value="<?php echo $review['id']; ?>">
<textarea name="comment" class="input" placeholder="Ваш ответ..." required maxlength="1000"></textarea>
<button type="submit" class="btn btn-primary btn-small"><i class="fas fa-paper-plane"></i> Отправить ответ</button>
<button type="button" class="btn btn-danger btn-small cancel-reply"><i class="fas fa-times"></i> Отмена</button>
</form>
<?php endif; ?>
</div>
<div class="review-edit-form" id="edit-form-<?php echo $review['id']; ?>" style="display: none;">
<h3>Редактировать отзыв</h3>
<?php if (!empty($review_success) && strpos($review_success, 'обновлен') !== false): ?><div class="alert alert-success"><?php echo escape($review_success); ?></div><?php endif; ?>
<?php if (!empty($review_error) && strpos($review_error, 'обновления') !== false): ?><div class="alert alert-error"><?php echo escape($review_error); ?></div><?php endif; ?>
<form action="site.php?id=<?php echo $site_id; ?>" method="POST">
<input type="hidden" name="action" value="edit_review">
<input type="hidden" name="review_id" value="<?php echo $review['id']; ?>">
<p>
<label for="edit_rating_<?php echo $review['id']; ?>">Репутация:</label>
<select id="edit_rating_<?php echo $review['id']; ?>" name="rating" class="input" required>
<?php for ($i = 5; $i >= -5; $i--): ?>
<option value="<?php echo $i; ?>" <?php echo ((int)$review['rating'] === $i) ? 'selected' : ''; ?>><?php echo $i > 0 ? '+' : ''; echo $i; ?></option>
<?php endfor; ?>
</select>
</p>
<p>
<label for="edit_comment_<?php echo $review['id']; ?>">Комментарий (до 1000 символов):</label>
<textarea id="edit_comment_<?php echo $review['id']; ?>" name="comment" class="input" maxlength="1000" required><?php echo escape($review['comment']); ?></textarea>
</p>
<?php if (!empty($images)): ?>
<div class="edit-images-section" style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee;">
<h4>Прикрепленные изображения:</h4>
<div class="edit-images-list" style="display: flex; flex-wrap: wrap; gap: 15px; align-items: center;">
<?php foreach ($images as $image): ?>
<?php
$image_path_clean = ltrim($image['image_path'], '/');
$image_url = SITE_URL . '/' . escape($image_path_clean);
?>
<div class="edit-image-item" style="border: 1px solid #ccc; padding: 5px; border-radius: 5px; text-align: center; background-color: #fff;">
<img src="<?php echo $image_url; ?>" alt="Изображение" style="width: 60px; height: 60px; object-fit: cover; display: block; margin: 0 auto 5px auto;">
<label style="font-size: 0.9em; color: #555;">
<input type="checkbox" name="delete_images[]" value="<?php echo $image['id']; ?>"> Удалить
</label>
</div>
<?php endforeach; ?>
</div>
</div>
<?php endif; ?>
<button type="submit" class="btn btn-primary btn-small"><i class="fas fa-save"></i> Сохранить</button>
<button type="button" class="btn btn-secondary btn-small cancel-edit"><i class="fas fa-times"></i> Отмена</button>
</form>
</div>
<?php if (!empty($review['replies'])): ?>
<div class="replies">
<?php displayReviews($review['replies'], $site_id, $loggedInUserId, $conn); ?>
</div>
<?php endif; ?>
</div>
<?php
}
}
$loggedInUserId = isLoggedIn() ? $_SESSION['user_id'] : null;
displayReviews($reviews, $site_id, $loggedInUserId, $conn);
?>
<?php endif; ?>
</div>
</div>
<div class="sidebar-right">
<h3>Категории сайтов</h3>
<?php foreach ($categories as $category): ?>
<a href="category.php?id=<?php echo $category['id']; ?>"><i class="fas fa-folder"></i> <?php echo escape($category['name']); ?></a> <?php endforeach; ?>
</div>
</div>
<div id="imageLightbox" class="lightbox-modal hidden">
<span class="close-lightbox">×</span>
<img class="lightbox-content" id="lightboxImage">
<div id="lightboxCaption"></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const tabs = document.querySelectorAll('.main-content .tab-container .tab');
const tabContents = document.querySelectorAll('.main-content .tab-content');
let dailyStatsChart = null;
let hourlyStatsChart = null;
function renderDailyStatsChart(graphData) {
const ctx = document.getElementById('dailyStatsChart');
if (!ctx) return;
if (dailyStatsChart) { dailyStatsChart.destroy(); }
if (!Array.isArray(graphData)) { console.error("Data for daily chart is not an array:", graphData); return; }
const labels = graphData.map(item => item.date);
const viewsData = graphData.map(item => item.views);
const uniqueData = graphData.map(item => item.unique_visitors);
dailyStatsChart = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{ label: 'Просмотры', data: viewsData, borderColor: 'rgb(75, 192, 192)', tension: 0.1, fill: false }, { label: 'Уникальные посетители', data: uniqueData, borderColor: 'rgb(255, 99, 132)', tension: 0.1, fill: false }]
},
options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { precision: 0 } }, x: { ticks: { maxRotation: 45, minRotation: 0, autoSkip: true, maxTicksLimit: 10 } } }, plugins: { legend: { display: true } } }
});
}
function renderHourlyStatsChart(graphData) {
const ctx = document.getElementById('hourlyStatsChart');
if (!ctx) return;
if (hourlyStatsChart) { hourlyStatsChart.destroy(); }
if (!Array.isArray(graphData)) { console.error("Data for hourly chart is not an array:", graphData); return; }
const labels = graphData.map(item => item.hour + ':00');
const viewsData = graphData.map(item => item.views);
const uniqueData = graphData.map(item => item.unique_visitors);
hourlyStatsChart = new Chart(ctx, {
type: 'bar',
data: {
labels: labels,
datasets: [{ label: 'Просмотры', data: viewsData, backgroundColor: 'rgba(75, 192, 192, 0.5)', borderColor: 'rgb(75, 192, 192)', borderWidth: 1 }, { label: 'Уникальные посетители', data: uniqueData, backgroundColor: 'rgba(255, 99, 132, 0.5)', borderColor: 'rgb(255, 99, 132)', borderWidth: 1 }]
},
options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { precision: 0 } }, x: { ticks: { maxRotation: 45, minRotation: 0, autoSkip: true, maxTicksLimit: 24 } } }, plugins: { legend: { display: true } } }
});
}
function activateTab(tabId) {
tabs.forEach(tab => { tab.classList.remove('active'); });
tabContents.forEach(content => { content.classList.remove('active'); content.style.display = 'none'; });
const activeTab = document.querySelector(`.main-content .tab[data-tab="${tabId}"]`);
const activeContent = document.getElementById(`${tabId}-content`);
if (activeTab) { activeTab.classList.add('active'); }
if (activeContent) { activeContent.classList.add('active'); activeContent.style.display = 'block'; }
localStorage.setItem('activeSiteTab_<?php echo $site_id; ?>', tabId);
if (tabId === 'graph') {
const dailyCanvas = document.getElementById('dailyStatsChart');
if (dailyCanvas) renderDailyStatsChart(<?php echo $daily_graph_data_json; ?>);
const hourlyCanvas = document.getElementById('hourlyStatsChart');
if (hourlyCanvas && <?php echo json_encode(!empty($hourly_graph_data)); ?>) { renderHourlyStatsChart(<?php echo $hourly_graph_data_json; ?>); hourlyCanvas.style.display = 'block'; } else if (hourlyCanvas) { hourlyCanvas.style.display = 'none'; }
} else {
if (dailyStatsChart) { dailyStatsChart.destroy(); dailyStatsChart = null; }
if (hourlyStatsChart) { hourlyStatsChart.destroy(); hourlyStatsChart = null; }
}
}
const urlParams = new URLSearchParams(window.location.search);
const tabFromUrl = urlParams.get('tab');
const hash = window.location.hash;
let initialTab = tabFromUrl || localStorage.getItem('activeSiteTab_<?php echo $site_id; ?>') || (hash && hash.startsWith('#review-') ? 'reviews-list' : 'stats');
activateTab(initialTab);
tabs.forEach(tab => {
tab.addEventListener('click', function() {
const tabId = this.getAttribute('data-tab');
activateTab(tabId);
});
});
const replyButtons = document.querySelectorAll('.reply-button');
const replyForms = document.querySelectorAll('.reply-form');
const cancelReplyButtons = document.querySelectorAll('.cancel-reply');
replyButtons.forEach(button => {
button.addEventListener('click', function() {
const reviewId = this.getAttribute('data-review-id');
const form = document.getElementById('reply-form-' + reviewId);
replyForms.forEach(f => f.style.display = 'none');
document.querySelectorAll('.review-edit-form').forEach(f => f.style.display = 'none');
document.querySelectorAll('.review-display').forEach(d => d.style.display = 'block');
if (form) {
form.style.display = 'block';
const textarea = form.querySelector('textarea');
if (textarea) { textarea.focus(); }
}
});
});
cancelReplyButtons.forEach(button => {
button.addEventListener('click', function() {
const form = this.closest('.reply-form');
if (form) { form.style.display = 'none'; const textarea = form.querySelector('textarea'); if (textarea) textarea.value = ''; }
});
});
const commentTextarea = document.getElementById('comment');
const reviewImageInput = document.getElementById('review_images');
const submitReviewButton = document.getElementById('submit-review-button');
const selectedFilesContainer = document.getElementById('selected-files-container');
if (commentTextarea && submitReviewButton) {
function checkSubmitButtonState() {
const filesSelected = reviewImageInput && reviewImageInput.files && reviewImageInput.files.length > 0;
const tooManyFilesSelected = filesSelected && reviewImageInput.files.length > 5;
if (commentTextarea.value.trim().length > 0 || (filesSelected && !tooManyFilesSelected)) {
submitReviewButton.disabled = false;
} else {
submitReviewButton.disabled = true;
}
if (selectedFilesContainer) {
const existingError = selectedFilesContainer.querySelector('.file-count-error');
if (tooManyFilesSelected) {
if (!existingError) {
const errorDiv = document.createElement('div');
errorDiv.classList.add('file-count-error');
errorDiv.style.color = 'red';
errorDiv.style.marginTop = '5px';
errorDiv.textContent = `Ошибка: Выбрано ${reviewImageInput.files.length} файлов, но максимум разрешено 5.`;
selectedFilesContainer.appendChild(errorDiv);
}
} else {
if (existingError) { existingError.remove(); }
}
}
}
commentTextarea.addEventListener('input', checkSubmitButtonState);
if (reviewImageInput && selectedFilesContainer) {
reviewImageInput.addEventListener('change', function() {
selectedFilesContainer.innerHTML = '';
const files = this.files;
if (files.length > 0) {
const fileList = document.createElement('ul');
fileList.style.listStyle = 'none';
fileList.style.padding = '0';
fileList.style.margin = '5px 0 0 0';
for (let i = 0; i < files.length; i++) {
const li = document.createElement('li');
li.style.marginBottom = '3px';
li.style.wordBreak = 'break-word';
const fileName = files[i].name;
const fileSizeMB = (files[i].size / 1024 / 1024).toFixed(2);
const fileType = files[i].type;
li.textContent = `Выбран: ${fileName} (${fileSizeMB} MB)`;
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
const maxSizeBytes = 5 * 1024 * 1024;
let errorMessages = [];
if (!allowedTypes.includes(fileType)) { errorMessages.push(`недопустимый тип (${fileType})`); }
if (files[i].size > maxSizeBytes) { errorMessages.push(`слишком большой размер (макс ${maxSizeBytes/1024/1024} MB)`); }
if (errorMessages.length > 0) { li.textContent += ' - Ошибка: ' + errorMessages.join(', '); li.style.color = 'red'; } else { li.style.color = '#555'; }
fileList.appendChild(li);
}
selectedFilesContainer.appendChild(fileList);
} else { selectedFilesContainer.innerHTML = ''; }
checkSubmitButtonState();
});
checkSubmitButtonState();
}
}
const editReviewButtons = document.querySelectorAll('.edit-review-button');
const cancelEditButtons = document.querySelectorAll('.review-edit-form .cancel-edit');
editReviewButtons.forEach(button => {
button.addEventListener('click', function() {
const reviewId = this.getAttribute('data-review-id');
const reviewDiv = document.getElementById('review-' + reviewId);
const reviewDisplay = reviewDiv.querySelector('.review-display');
const editFormDiv = reviewDiv.querySelector('.review-edit-form');
if (reviewDisplay && editFormDiv) {
replyForms.forEach(f => f.style.display = 'none');
document.querySelectorAll('.review-edit-form').forEach(f => f.style.display = 'none');
document.querySelectorAll('.review-display').forEach(d => d.style.display = 'block');
reviewDisplay.style.display = 'none';
editFormDiv.style.display = 'block';
const textarea = editFormDiv.querySelector('textarea');
if (textarea) { textarea.focus(); }
}
});
});
cancelEditButtons.forEach(button => {
button.addEventListener('click', function() {
const editFormDiv = this.closest('.review-edit-form');
const reviewDiv = editFormDiv.closest('.review');
const reviewDisplay = reviewDiv.querySelector('.review-display');
if (reviewDisplay && editFormDiv) {
editFormDiv.style.display = 'none';
reviewDisplay.style.display = 'block';
const alertErrors = editFormDiv.querySelectorAll('.alert-error');
alertErrors.forEach(alert => alert.remove());
const alertSuccess = editFormDiv.querySelectorAll('.alert-success');
alertSuccess.forEach(alert => alert.remove());
}
});
});
const lightbox = document.getElementById('imageLightbox');
const lightboxImg = document.getElementById('lightboxImage');
const closeLightbox = document.querySelector('.close-lightbox');
document.addEventListener('click', function(event) {
const thumbnail = event.target.closest('.review-thumbnail');
if (thumbnail) {
const fullImgUrl = thumbnail.getAttribute('data-full-img-url');
lightbox.classList.remove('hidden');
lightbox.style.display = 'flex';
lightboxImg.src = fullImgUrl;
event.preventDefault();
}
});
if (closeLightbox) {
closeLightbox.addEventListener('click', function() {
lightbox.style.display = '';
lightbox.classList.add('hidden');
lightboxImg.src = '';
});
}
if (lightbox) {
lightbox.addEventListener('click', function(event) {
if (event.target === lightbox) {
lightbox.style.display = '';
lightbox.classList.add('hidden');
lightboxImg.src = '';
}
});
}
});
</script>
</body>
</html>
<?php
$conn->close();
?>