Сделано грамотное мягкое удаление комментариев родителей с ответами в ветках

This commit is contained in:
2026-01-17 13:12:43 +05:00
parent 8d4b593beb
commit a1ef4c2358
6 changed files with 373 additions and 288 deletions

View File

@@ -969,7 +969,9 @@ function commentPostDelete($comment_id)
$comment_id = (int)$comment_id;
if ($comment_id <= 0) die('Ошибка: Неверный ID');
// --- ПОЛУЧЕНИЕ ДАННЫХ И ПРОВЕРКА ПРАВ ---
// Читаем тип удаления из запроса (для админки)
$delete_type = $_REQUEST['admin_action_type'] ?? 'auto';
$this->_commentSettingsGet();
$comment_data = $AVE_DB->Query("
@@ -985,7 +987,6 @@ function commentPostDelete($comment_id)
$anon_key = $this->_getAnonKey();
$can_delete = false;
if ($user_group === 1) {
$can_delete = true;
} else {
@@ -993,10 +994,7 @@ function commentPostDelete($comment_id)
($comment_data['comment_author_id'] == 0 && !empty($comment_data['anon_key']) && $comment_data['anon_key'] == $anon_key);
$is_time_ok = (time() - (int)$comment_data['comment_published'] < (int)$this->conf_edit_time);
if ($is_author && $is_time_ok) {
$can_delete = true;
}
if ($is_author && $is_time_ok) $can_delete = true;
}
if (!$can_delete) {
@@ -1006,10 +1004,13 @@ function commentPostDelete($comment_id)
$has_children = $AVE_DB->Query("SELECT COUNT(*) FROM " . PREFIX . "_module_comment_info WHERE parent_id = '" . $comment_id . "'")->GetCell();
$upload_dir = BASE_DIR . '/uploads/comments/';
// Проверка на Ajax
$is_ajax = (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest');
// --- МЯГКОЕ УДАЛЕНИЕ (если есть ответы и удаляет не админ) ---
if ($has_children > 0 && $user_group != 1) {
// Удаляем файлы физически, если они есть
// --- ЛОГИКА МЯГКОГО УДАЛЕНИЯ ---
if ($has_children > 0 && $delete_type !== 'full') {
// 1. Физически удаляем файлы, если они были
if (!empty($comment_data['comment_file'])) {
$files_to_del = explode(',', $comment_data['comment_file']);
foreach ($files_to_del as $f_name) {
@@ -1018,22 +1019,31 @@ function commentPostDelete($comment_id)
}
}
$del_text = "<span class='comment-deleted'>Комментарий удален автором.</span>";
$status_marker = ($user_group === 1) ? '__DEL_BY_ADM__' : '__DEL_BY_AUT__';
$AVE_DB->Query("
UPDATE " . PREFIX . "_module_comment_info
SET comment_text = '" . addslashes($del_text) . "',
SET comment_text = '" . $status_marker . "',
comment_author_name = '__DELETED__',
comment_author_id = '0',
comment_author_email = '',
comment_author_ip = '0.0.0.0',
comment_file = '',
anon_key = '',
user_rating = '0',
rating_sum = '0',
rating_count = '0',
comment_status = '1',
comment_changed = '" . time() . "'
WHERE Id = '" . $comment_id . "'
");
echo "OK_SOFT";
echo $is_ajax ? "OK_SOFT" : "OK";
}
else {
// --- ПОЛНОЕ УДАЛЕНИЕ (Админ или нет вложенных ответов) ---
// --- ПОЛНОЕ УДАЛЕНИЕ ---
$ids_to_delete = [$comment_id];
// Если админ — собираем всю ветку детей для удаления
if ($user_group == 1) {
$all_child_ids = [];
$parent_ids = [$comment_id];
@@ -1053,7 +1063,6 @@ function commentPostDelete($comment_id)
$final_ids_str = implode(',', $ids_to_delete);
// Массовое удаление всех файлов во всех удаляемых комментариях
$files_res = $AVE_DB->Query("SELECT comment_file FROM " . PREFIX . "_module_comment_info WHERE Id IN ($final_ids_str)");
while ($f = $files_res->FetchAssocArray()) {
if (!empty($f['comment_file'])) {
@@ -1065,7 +1074,6 @@ function commentPostDelete($comment_id)
}
}
// Удаляем записи из БД
$AVE_DB->Query("DELETE FROM " . PREFIX . "_module_comment_info WHERE Id IN ($final_ids_str)");
echo "OK";
@@ -1460,13 +1468,47 @@ function commentAdminListShow($tpl_dir)
$all_items[$row['Id']] = $row;
}
// --- ПОСТРОЕНИЕ ДЕРЕВА ---
// --- ПОСТРОЕНИЕ ДЕРЕВА ---
$child_map = [];
foreach ($all_items as $id => $item) {
$p_id = (int)$item['parent_id'];
if ($p_id > 0) $child_map[$p_id][] = $id;
}
// Проходим по всем элементам и помечаем тех, у кого есть дети в child_map
foreach ($all_items as $id => &$item_for_check) {
$item_for_check['has_children'] = isset($child_map[$id]) ? 1 : 0;
}
unset($item_for_check); // очистка ссылки
$docs = [];
$processed = [];
$buildTree = function($parent_id, $level) use (&$buildTree, &$docs, &$processed, &$all_items, &$child_map) {
if (isset($child_map[$parent_id])) {
foreach ($child_map[$parent_id] as $child_id) {
if (!in_array($child_id, $processed)) {
$item = $all_items[$child_id];
$item['depth_level'] = $level;
// has_children уже записан в $all_items выше
$docs[] = $item;
$processed[] = $child_id;
$buildTree($child_id, $level + 1);
}
}
}
};
foreach ($all_items as $id => $item) {
if ($item['parent_id'] == 0 && !in_array($id, $processed)) {
$item['depth_level'] = 0;
$docs[] = $item;
$processed[] = $id;
$buildTree($id, 1);
}
}
$docs = [];
$processed = [];

View File

@@ -290,3 +290,11 @@ html {
font-weight: normal;
margin-left: 5px;
}
/* Стиль для кнопки удаления всей ветки */
.ico_delete_all {
background-position: -48px -48px; /* Используем стандартную иконку крестика, но... */
filter: hue-rotate(140deg) saturate(3); /* ...меняем её цвет на фиолетово-бордовый */
opacity: 0.8;
}
.ico_delete_all:hover { opacity: 1; transform: scale(1.1); }

View File

@@ -178,84 +178,82 @@ if ($fileInput.length && $fileInput[0].files.length > 0) {
}
/* --- ДЕЙСТВИЯ (DELETE, LOCK, OPEN/CLOSE, VOTE) --- */
function cAction(obj, action) {
var $btn = $(obj);
var cid = $btn.data('id');
if (!cid && action !== 'open' && action !== 'close') return;
$.get(aveabspath + 'index.php', {
module: 'comment',
action: action,
docid: typeof DOC_ID !== 'undefined' ? DOC_ID : '',
Id: cid ? cid : ''
}, function() {
if (action === 'close') {
$btn.attr('id', 'mod_comment_open')
.removeClass('btn-outline-danger').addClass('btn-outline-success')
.html('<i class="bi bi-lock-fill me-1"></i> ' + COMMENT_SITE_OPEN);
$('#mod_comment_new').fadeOut(300);
}
else if (action === 'open') {
$btn.attr('id', 'mod_comment_close')
.removeClass('btn-outline-success').addClass('btn-outline-danger')
.html('<i class="bi bi-unlock-fill me-1"></i> ' + COMMENT_SITE_CLOSE);
$('#mod_comment_new').fadeIn(300);
}
if (action === 'delete') {
var $commentBlock = $btn.closest('.mod_comment_comment');
$commentBlock.fadeOut(300, function() { $(this).remove(); });
}
// показать / скрыть комментарий
if (action === 'unlock' || action === 'lock') {
var $icon = $btn.find('i');
function cAction(obj, action) {
var $btn = $(obj);
var cid = $btn.data('id');
// ищем внутренний бокс, чтобы не красить детей
var $commentBox = $btn.closest('.mod_comment_box');
// поиск всей карточки для очистки старых стилей
var $card = $btn.closest('.mod_comment_comment');
if (action === 'lock') {
// --- Статус 0: СКРЫТО ---
$icon.attr('class', 'bi bi-eye-slash');
$btn.removeClass('text-success text-muted').addClass('text-danger').attr('title', COMMENT_ICON_SHOW);
if (!cid && action !== 'open' && action !== 'close') return;
$.get(aveabspath + 'index.php', {
module: 'comment',
action: action,
docid: typeof DOC_ID !== 'undefined' ? DOC_ID : '',
Id: cid ? cid : ''
}, function(data) { // Добавляем аргумент data для получения ответа от PHP
// красим внутренний блок сообщения
$commentBox.addClass('opacity-75 border border-warning rounded');
// текст
var $alert = $commentBox.find('.alert-warning');
if ($alert.length === 0) {
var alertHtml = '<div class="alert alert-warning py-1 px-2 mb-2 small d-flex align-items-center border-0 shadow-sm" style="border-left: 4px solid #ffc107 !important; display:none;">' +
'<i class="bi bi-clock-history me-2 text-dark"></i>' +
'<span class="text-dark">' + COMMENT_WAITING_MODERATION + '</span>' +
'</div>';
// Вставляем в именно этого сообщения
$commentBox.find('.flex-grow-1').first().prepend(alertHtml);
$alert = $commentBox.find('.alert-warning');
if (action === 'close') {
$btn.attr('id', 'mod_comment_open')
.removeClass('btn-outline-danger').addClass('btn-outline-success')
.html('<i class="bi bi-lock-fill me-1"></i> ' + COMMENT_SITE_OPEN);
$('#mod_comment_new').fadeOut(300);
}
else if (action === 'open') {
$btn.attr('id', 'mod_comment_close')
.removeClass('btn-outline-success').addClass('btn-outline-danger')
.html('<i class="bi bi-unlock-fill me-1"></i> ' + COMMENT_SITE_CLOSE);
$('#mod_comment_new').fadeIn(300);
}
$alert.stop(true, true).fadeIn(300);
} else {
// --- Статус 1: ВИДИМО ---
$icon.attr('class', 'bi bi-eye');
$btn.removeClass('text-danger text-muted').addClass('text-success').attr('title', COMMENT_ICON_HIDE);
// чистим бокс и карточку
var classesToRemove = 'opacity-75 border border-warning rounded';
$commentBox.removeClass(classesToRemove);
$card.removeClass(classesToRemove + ' border-warning'); // Чистим старый класс из твоего кода
// удаляем плашку:
$commentBox.find('.alert-warning').first().stop(true, true).fadeOut(300, function() {
$(this).remove();
});
}
}
// --- ОБНОВЛЕННЫЙ БЛОК УДАЛЕНИЯ ---
if (action === 'delete') {
var response = data ? data.trim() : '';
var $commentBlock = $btn.closest('.mod_comment_comment'); // Вся ветка (включая детей)
var $commentBox = $btn.closest('.mod_comment_box'); // Только текущая карточка
if (response === 'OK_SOFT') {
// обновляем страницу
location.reload();
} else {
// удаляем совсем комментарий
$commentBlock.fadeOut(300, function() { $(this).remove(); });
}
}
// показать / скрыть комментарий
if (action === 'unlock' || action === 'lock') {
var $icon = $btn.find('i');
var $commentBox = $btn.closest('.mod_comment_box');
var $card = $btn.closest('.mod_comment_comment');
if (action === 'lock') {
$icon.attr('class', 'bi bi-eye-slash');
$btn.removeClass('text-success text-muted').addClass('text-danger').attr('title', COMMENT_ICON_SHOW);
$commentBox.addClass('opacity-75 border border-warning rounded');
var $alert = $commentBox.find('.alert-warning');
if ($alert.length === 0) {
var alertHtml = '<div class="alert alert-warning py-1 px-2 mb-2 small d-flex align-items-center border-0 shadow-sm" style="border-left: 4px solid #ffc107 !important; display:none;">' +
'<i class="bi bi-clock-history me-2 text-dark"></i>' +
'<span class="text-dark">' + COMMENT_WAITING_MODERATION + '</span>' +
'</div>';
$commentBox.find('.flex-grow-1').first().prepend(alertHtml);
$alert = $commentBox.find('.alert-warning');
}
$alert.stop(true, true).fadeIn(300);
} else {
$icon.attr('class', 'bi bi-eye');
$btn.removeClass('text-danger text-muted').addClass('text-success').attr('title', COMMENT_ICON_HIDE);
var classesToRemove = 'opacity-75 border border-warning rounded';
$commentBox.removeClass(classesToRemove);
$card.removeClass(classesToRemove + ' border-warning');
$commentBox.find('.alert-warning').first().stop(true, true).fadeOut(300, function() {
$(this).remove();
});
}
}
});
}
/* --- ИНИЦИАЛИЗАЦИЯ --- */
$(document).ready(function() {

View File

@@ -122,6 +122,10 @@ COMMENT_JS_ERR_SRV = "Ошибка связи с сервером"
COMMENT_WAITING_MODERATION = "Ваш комментарий на проверке и виден пока только вам."
COMMENT_LAST_TITEL = "Последние комментарии"
COMMENT_HIDDEN_BY_MODERATOR = "Комментарий скрыт модератором и находится на проверке."
COMMENT_TEXT_DEL_BY_ADMIN = "Комментарий удален администратором."
COMMENT_TEXT_DEL_BY_AUTHOR = "Комментарий удален автором."
COMMENT_STATUS_DELETED = "Удалено"
[admin]

View File

@@ -125,18 +125,29 @@
</a>
<a href="{$ABS_PATH}index.php?id={$row.document_id}#comment-{$row.CId}" target="_blank" style="color:#27ae60; font-weight:bold;">[#] Посмотреть</a>
<div style="display: flex; gap: 8px; align-items: center; border-left: 1px solid #ddd; padding-left: 10px;">
<span style="font-weight:bold; color:{if $row.comment_status=='1'}#27ae60{else}#e74c3c{/if};">
{if $row.comment_status=='1'}Одобрен{else}Скрыт{/if}
</span>
{if $row.comment_status == '1'}
<a class="icon_sprite ico_unlock_no" title="Скрыть" href="index.php?do=modules&action=modedit&mod=comment&moduleaction=1&admin_action=set_status_0&id={$row.CId}&cp={$sess}"></a>
{else}
<a class="icon_sprite ico_unlock" title="Одобрить" href="index.php?do=modules&action=modedit&mod=comment&moduleaction=1&admin_action=set_status_1&id={$row.CId}&cp={$sess}"></a>
{/if}
<a class="icon_sprite ico_edit" title="Редактировать" href="javascript:void(0);" onClick="windowOpen('index.php?do=modules&action=modedit&mod=comment&moduleaction=admin_edit&pop=1&docid={$row.document_id}&Id={$row.CId}','700','700','1');"></a>
<a class="icon_sprite ico_delete ConfirmDelete" title="Удалить" href="index.php?do=modules&action=modedit&mod=comment&moduleaction=1&admin_action=delete&id={$row.CId}&cp={$sess}"></a>
</div>
<div style="display: flex; gap: 8px; align-items: center; border-left: 1px solid #ddd; padding-left: 10px;">
<span style="font-weight:bold; color:{if $row.comment_status=='1'}#27ae60{else}#e74c3c{/if};">
{if $row.comment_status=='1'}Одобрен{else}Скрыт{/if}
</span>
{if $row.comment_status == '1'}
<a class="icon_sprite ico_unlock_no" title="Скрыть" href="index.php?do=modules&action=modedit&mod=comment&moduleaction=1&admin_action=set_status_0&id={$row.CId}&cp={$sess}"></a>
{else}
<a class="icon_sprite ico_unlock" title="Одобрить" href="index.php?do=modules&action=modedit&mod=comment&moduleaction=1&admin_action=set_status_1&id={$row.CId}&cp={$sess}"></a>
{/if}
<a class="icon_sprite ico_edit" title="Редактировать" href="javascript:void(0);" onClick="windowOpen('index.php?do=modules&action=modedit&mod=comment&moduleaction=admin_edit&pop=1&docid={$row.document_id}&Id={$row.CId}','700','700','1');"></a>
<a class="icon_sprite ico_delete ConfirmDelete"
title="{if $row.has_children}Удалить только этот текст (сохранить ветку){else}Удалить физически{/if}"
href="index.php?do=modules&action=modedit&mod=comment&moduleaction=1&admin_action=delete&admin_action_type=soft&id={$row.CId}&cp={$sess}"></a>
{if $row.has_children}
<a class="icon_sprite ico_delete ico_delete_all ConfirmDelete"
title="УДАЛИТЬ ВСЮ ВЕТКУ ФИЗИЧЕСКИ (вместе с ответами)"
href="index.php?do=modules&action=modedit&mod=comment&moduleaction=1&admin_action=delete&admin_action_type=full&id={$row.CId}&cp={$sess}"></a>
{/if}
</div>
</div>
<div class="comment-scroll-box">{$row.comment_text|nl2br}</div>

View File

@@ -1,5 +1,11 @@
{foreach from=$subcomments item=c name=sub_loop}
<div class="card mb-3 mod_comment_comment {if $c.parent_id} ms-2 ms-md-4{/if}"
{* ОПРЕДЕЛЯЕМ СТАТУС УДАЛЕНИЯ *}
{assign var="is_deleted" value=false}
{if $c.comment_author_name == '__DELETED__' || $c.comment_author_name == 'Удалено'}
{assign var="is_deleted" value=true}
{/if}
<div class="card mb-3 mod_comment_comment {if $c.parent_id} ms-2 ms-md-4{/if} {if $is_deleted}opacity-75 bg-light{/if}"
id="comment_wrapper_{$c.Id}"
data-parent="{$c.parent_id|default:0}"
data-user-rating="{$c.user_rating|default:0}"
@@ -9,215 +15,232 @@
<div class="border border-warning border-3 rounded p-0">
{/if}
<div id="comment_{$c.Id}" class="mod_comment_box {if $c.comment_status == 0}opacity-75 border border-warning rounded{/if}">
<div id="comment_{$c.Id}" class="mod_comment_box {if $c.comment_status == 0 && !$is_deleted}opacity-75 border border-warning rounded{/if}">
<div class="card-body p-3 d-flex align-items-start">
{* БЛОК АВАТАРА *}
<div class="mod_comment_avatar">
{if isset($c.avatar) && $c.avatar != ''}
<img src="{$c.avatar}" alt="{$c.comment_author_name|escape}" class="rounded-circle">
{else}
<div class="mod_comment_avatar_letter av-c{$c.avatar_color_index|default:1}">
{$c.first_letter|default:'?'|upper}
</div>
{/if}
{if !$is_deleted}
{if isset($c.avatar) && $c.avatar != ''}
<img src="{$c.avatar}" alt="{$c.comment_author_name|escape}" class="rounded-circle">
{else}
<div class="mod_comment_avatar_letter av-c{$c.avatar_color_index|default:1}">
{$c.first_letter|default:'?'|upper}
</div>
{/if}
{else}
<div class="mod_comment_avatar_letter bg-secondary text-white" style="opacity: 0.5;">
{if $c.comment_text == '__DEL_BY_ADM__'}
<i class="bi bi-shield-lock"></i>
{else}
<i class="bi bi-person-x"></i>
{/if}
</div>
{/if}
</div>
<div class="flex-grow-1 min-w-0">
{* ПРОВЕРКА НА МОДЕРАЦИЮ ДЛЯ АВТОРА *}
{if $c.comment_status == 0}
<div class="alert alert-warning py-1 px-2 mb-2 small d-flex align-items-center border-0 shadow-sm" style="border-left: 4px solid #ffc107 !important;">
<i class="bi bi-clock-history me-2 text-dark"></i>
<span class="text-dark">
{#COMMENT_WAITING_MODERATION#}
</span>
</div>
{/if}
{* Информация об авторе и дате *}
<div class="mod_comment_meta text-muted small d-flex flex-wrap align-items-center">
<span class="d-flex align-items-center">
<i class="bi bi-person me-1"></i>
<a title="{#COMMENT_INFO#}" href="javascript:void(0);" onclick="popup('{$ABS_PATH}index.php?module=comment&action=postinfo&pop=1&Id={$c.Id}&theme={$theme}','comment','500','300','1');" class="fw-bold link-dark text-decoration-none">{$c.comment_author_name|stripslashes|escape}</a>
{* ПРОВЕРКА ИСТОРИИ ИМЕН АНОНИМА *}
{if !empty($c.past_names)}
<span class="badge rounded-pill bg-light text-danger border border-danger ms-1"
style="font-size: 0.65rem; cursor: help;"
data-bs-toggle="tooltip"
data-bs-html="true"
title="{#COMMENT_CHECK_NAME#}<br/>{foreach $c.past_names as $pname}{$pname|escape}<br/>{/foreach}">
<i class="bi bi-exclamation-triangle-fill"></i> {#COMMENT_CHECK_NAME_TRUE#}
</span>
{/if}
</span>
<span class="d-flex align-items-center"><i class="bi bi-clock me-1"></i> {$c.comment_published}</span>
{if $smarty.const.UGROUP==1}
<span class="text-secondary d-none d-sm-inline">• IP:{$c.comment_author_ip}</span>
{/if}
{if isset($c.comment_changed) && $c.comment_changed && $c.comment_changed != '0'}
<span class="badge bg-light text-secondary border fw-normal" id="changed_{$c.Id}">{#COMMENT_CHECK_NAME_EDIT#} {$c.comment_changed}</span>
{/if}
</div>
{* Текст комментария *}
<div class="mod_comment_text comment-text-content mb-2">
{$c.comment_text|stripslashes|nl2br}
</div>
{* Доп. поля (Сайт, Город) *}
{if $c.comment_author_website || $c.comment_author_city}
<div class="small text-muted mb-2 border-start ps-2">
{if $c.comment_author_website}<div><i class="bi bi-link-45deg"></i> {$c.comment_author_website}</div>{/if}
{if $c.comment_author_city}<div><i class="bi bi-geo-alt"></i> {$c.comment_author_city}</div>{/if}
</div>
{* ПРОВЕРКА НА МОДЕРАЦИЮ ДЛЯ АВТОРА *}
{if $c.comment_status == 0 && !$is_deleted}
<div class="alert alert-warning py-1 px-2 mb-2 small d-flex align-items-center border-0 shadow-sm" style="border-left: 4px solid #ffc107 !important;">
<i class="bi bi-clock-history me-2 text-dark"></i>
<span class="text-dark">
{#COMMENT_WAITING_MODERATION#}
</span>
</div>
{/if}
{* Вывод файлов (Картинки ПЕРВЫМИ, остальное в КОНЦЕ) *}
{if !empty($c.comment_file)}
<div class="mod_comment_attached_images mt-2 d-flex flex-wrap gap-2" id="image_container_{$c.Id}">
{assign var="all_files" value=","|explode:$c.comment_file}
{assign var="img_exts" value=['jpg', 'jpeg', 'png', 'gif', 'webp']}
{* Информация об авторе и дате *}
<div class="mod_comment_meta text-muted small d-flex flex-wrap align-items-center">
{if !$is_deleted}
<span class="d-flex align-items-center">
<i class="bi bi-person me-1"></i>
<a title="{#COMMENT_INFO#}" href="javascript:void(0);" onclick="popup('{$ABS_PATH}index.php?module=comment&action=postinfo&pop=1&Id={$c.Id}&theme={$theme}','comment','500','300','1');" class="fw-bold link-dark text-decoration-none">{$c.comment_author_name|stripslashes|escape}</a>
{* ПРОВЕРКА ИСТОРИИ ИМЕН АНОНИМА *}
{if !empty($c.past_names)}
<span class="badge rounded-pill bg-light text-danger border border-danger ms-1"
style="font-size: 0.65rem; cursor: help;"
data-bs-toggle="tooltip"
data-bs-html="true"
title="{#COMMENT_CHECK_NAME#}<br/>{foreach $c.past_names as $pname}{$pname|escape}<br/>{/foreach}">
<i class="bi bi-exclamation-triangle-fill"></i> {#COMMENT_CHECK_NAME_TRUE#}
</span>
{/if}
</span>
<span class="d-flex align-items-center ms-2"><i class="bi bi-clock me-1"></i> {$c.comment_published}</span>
{* 1. Сначала выводим только ИЗОБРАЖЕНИЯ *}
{foreach from=$all_files item=file}
{assign var="f_name" value=$file|trim}
{if $f_name}
{assign var="ext_parts" value="."|explode:$f_name}
{assign var="f_ext" value=$ext_parts[$ext_parts|@count-1]|lower}
{if in_array($f_ext, $img_exts)}
<div class="comment-image-item d-flex flex-column align-items-center justify-content-between h-100">
<a href="{$ABS_PATH}uploads/comments/{$f_name}" target="_blank" class="card-link-wrapper d-flex align-items-center justify-content-center mb-1" style="height: 100px;">
<img src="{$ABS_PATH}uploads/comments/{$f_name}"
class="img-fluid rounded border shadow-sm"
style="width: 100px; height: 100px; object-fit: cover;"
alt="{#COMMENT_FILE_IMAGE#}" />
</a>
<div class="d-flex align-items-center">
<span class="badge bg-light text-secondary border fw-normal text-truncate d-inline-block"
style="max-width: 100px; font-size: 0.7rem; padding: 1px 4px; line-height: 1.2;"
title="{$f_name|regex_replace:'/_[0-9]+(?=\.[a-z0-9]+$)/i':''}">
{$f_name|regex_replace:"/_[0-9]+(?=\.[a-z0-9]+$)/i":""}
</span>
</div>
</div>
{/if}
{if $smarty.const.UGROUP==1}
<span class="text-secondary d-none d-sm-inline ms-2">• IP:{$c.comment_author_ip}</span>
{/if}
{if isset($c.comment_changed) && $c.comment_changed && $c.comment_changed != '0'}
<span class="badge bg-light text-secondary border fw-normal ms-2" id="changed_{$c.Id}">{#COMMENT_CHECK_NAME_EDIT#} {$c.comment_changed}</span>
{/if}
{else}
<span class="fw-bold text-secondary text-uppercase" style="font-size: 0.75rem;">
<i class="bi bi-info-circle me-1"></i> {#COMMENT_STATUS_DELETED#}
</span>
<span class="ms-2 d-flex align-items-center"><i class="bi bi-clock me-1"></i> {$c.comment_published}</span>
{/if}
</div>
{* Текст комментария *}
<div class="mod_comment_text comment-text-content mb-2 {if $is_deleted}text-secondary fst-italic{/if}">
{if $c.comment_text == '__DEL_BY_ADM__'}
<i class="bi bi-shield-exclamation me-1"></i> {#COMMENT_TEXT_DEL_BY_ADMIN#}
{elseif $c.comment_text == '__DEL_BY_AUT__'}
<i class="bi bi-trash3 me-1"></i> {#COMMENT_TEXT_DEL_BY_AUTHOR#}
{elseif $is_deleted}
<i class="bi bi-trash3 me-1"></i> {#COMMENT_STATUS_DELETED#}
{else}
{$c.comment_text|stripslashes|nl2br}
{/if}
{/foreach}
{* 2. Затем выводим все ОСТАЛЬНЫЕ файлы *}
{foreach from=$all_files item=file}
{assign var="f_name" value=$file|trim}
{if $f_name}
{assign var="ext_parts" value="."|explode:$f_name}
{assign var="f_ext" value=$ext_parts[$ext_parts|@count-1]|lower}
{if !in_array($f_ext, $img_exts)}
<div class="comment-image-item d-flex flex-column align-items-center justify-content-between h-100">
<a href="{$ABS_PATH}uploads/comments/{$f_name}" target="_blank" class="card-link-wrapper text-decoration-none d-block text-center mb-1">
<div class="file-box d-flex align-items-center justify-content-center bg-light text-dark rounded border shadow-sm"
style="width: 100px; height: 100px; font-weight: bold; text-transform: uppercase; font-size: 14px; transition: all 0.2s;">
<i class="bi bi-file-earmark-arrow-down me-1"></i>{$f_ext}
</div>
</a>
<div class="d-flex align-items-center">
<span class="badge bg-light text-secondary border fw-normal text-truncate d-inline-block"
style="max-width: 100px; font-size: 0.7rem; padding: 1px 4px; line-height: 1.2;"
title="{$f_name|regex_replace:'/_[0-9]+(?=\.[a-z0-9]+$)/i':''}">
{$f_name|regex_replace:"/_[0-9]+(?=\.[a-z0-9]+$)/i":""}
</span>
</div>
</div>
{/if}
{/if}
{/foreach}
</div>
{/if}
{* Доп. поля и Файлы (Выводим только если НЕ удален) *}
{if !$is_deleted}
{if $c.comment_author_website || $c.comment_author_city}
<div class="small text-muted mb-2 border-start ps-2">
{if $c.comment_author_website}<div><i class="bi bi-link-45deg"></i> {$c.comment_author_website}</div>{/if}
{if $c.comment_author_city}<div><i class="bi bi-geo-alt"></i> {$c.comment_author_city}</div>{/if}
</div>
{/if}
{if !empty($c.comment_file)}
<div class="mod_comment_attached_images mt-2 d-flex flex-wrap gap-2" id="image_container_{$c.Id}">
{assign var="all_files" value=","|explode:$c.comment_file}
{assign var="img_exts" value=['jpg', 'jpeg', 'png', 'gif', 'webp']}
{* 1. ИЗОБРАЖЕНИЯ *}
{foreach from=$all_files item=file}
{assign var="f_name" value=$file|trim}
{if $f_name}
{assign var="ext_parts" value="."|explode:$f_name}
{assign var="f_ext" value=$ext_parts[$ext_parts|@count-1]|lower}
{if in_array($f_ext, $img_exts)}
<div class="comment-image-item d-flex flex-column align-items-center justify-content-between h-100">
<a href="{$ABS_PATH}uploads/comments/{$f_name}" target="_blank" class="card-link-wrapper d-flex align-items-center justify-content-center mb-1" style="height: 100px;">
<img src="{$ABS_PATH}uploads/comments/{$f_name}" class="img-fluid rounded border shadow-sm" style="width: 100px; height: 100px; object-fit: cover;" alt="{#COMMENT_FILE_IMAGE#}" />
</a>
<div class="d-flex align-items-center">
<span class="badge bg-light text-secondary border fw-normal text-truncate d-inline-block" style="max-width: 100px; font-size: 0.7rem; padding: 1px 4px; line-height: 1.2;" title="{$f_name|regex_replace:'/_[0-9]+(?=\.[a-z0-9]+$)/i':''}">
{$f_name|regex_replace:"/_[0-9]+(?=\.[a-z0-9]+$)/i":""}
</span>
</div>
</div>
{/if}
{/if}
{/foreach}
{* 2. ОСТАЛЬНЫЕ файлы *}
{foreach from=$all_files item=file}
{assign var="f_name" value=$file|trim}
{if $f_name}
{assign var="ext_parts" value="."|explode:$f_name}
{assign var="f_ext" value=$ext_parts[$ext_parts|@count-1]|lower}
{if !in_array($f_ext, $img_exts)}
<div class="comment-image-item d-flex flex-column align-items-center justify-content-between h-100">
<a href="{$ABS_PATH}uploads/comments/{$f_name}" target="_blank" class="card-link-wrapper text-decoration-none d-block text-center mb-1">
<div class="file-box d-flex align-items-center justify-content-center bg-light text-dark rounded border shadow-sm" style="width: 100px; height: 100px; font-weight: bold; text-transform: uppercase; font-size: 14px; transition: all 0.2s;">
<i class="bi bi-file-earmark-arrow-down me-1"></i>{$f_ext}
</div>
</a>
<div class="d-flex align-items-center">
<span class="badge bg-light text-secondary border fw-normal text-truncate d-inline-block" style="max-width: 100px; font-size: 0.7rem; padding: 1px 4px; line-height: 1.2;" title="{$f_name|regex_replace:'/_[0-9]+(?=\.[a-z0-9]+$)/i':''}">
{$f_name|regex_replace:"/_[0-9]+(?=\.[a-z0-9]+$)/i":""}
</span>
</div>
</div>
{/if}
{/if}
{/foreach}
</div>
{/if}
{/if}
</div>
</div>
{* ФУТЕР *}
<div class="card-footer bg-white border-top p-2 d-flex flex-wrap justify-content-between align-items-center gap-2">
<div class="d-flex align-items-center flex-wrap gap-2">
{* 1. Оценка автора *}
{if isset($c.user_rating) && $c.user_rating > 0}
<div class="rating-author-badge" title="{#COMMENT_AUTOR_RATING_A#} {$c.user_rating} {#COMMENT_AUTOR_RATING_IZ#} 5">
<span class="d-none d-sm-inline">{#COMMENT_AUTOR_RATING_B#}</span>
<div class="rating-author-stars">
{section name=r_star start=1 loop=6}
<i class="bi {if $smarty.section.r_star.index <= $c.user_rating}bi-star-fill{else}bi-star{/if}"></i>
{/section}
{if !$is_deleted}
{* 1. Оценка автора *}
{if isset($c.user_rating) && $c.user_rating > 0}
<div class="rating-author-badge" title="{#COMMENT_AUTOR_RATING_A#} {$c.user_rating} {#COMMENT_AUTOR_RATING_IZ#} 5">
<span class="d-none d-sm-inline">{#COMMENT_AUTOR_RATING_B#}</span>
<div class="rating-author-stars">
{section name=r_star start=1 loop=6}
<i class="bi {if $smarty.section.r_star.index <= $c.user_rating}bi-star-fill{else}bi-star{/if}"></i>
{/section}
</div>
<span class="rating-value">{$c.user_rating}</span>
</div>
<span class="rating-value">{$c.user_rating}</span>
</div>
{/if}
{/if}
{* 2. Рейтинг комментария *}
{if $comment_rating_type != 2}
<div class="comment-rating-container d-flex align-items-center py-1 px-2 bg-light rounded border" data-id="{$c.Id}">
{if $comment_rating_type == 1}
<div class="comment-like text-danger" style="cursor: pointer;">
<i class="star-item bi bi-heart-fill me-1" data-value="5"></i>
<span class="fw-bold small">{$c.rating_count|default:0}</span>
</div>
{else}
<div class="comment-stars text-warning" style="cursor: pointer;">
{assign var="avg_rating" value=0}
{if isset($c.rating_count) && $c.rating_count > 0}
{math equation="round(x / y)" x=$c.rating_sum y=$c.rating_count assign="avg_rating"}
{/if}
<div class="d-flex align-items-center me-1">
{section name=star start=1 loop=6}
<i class="star-item bi {if $smarty.section.star.index <= $avg_rating}bi-star-fill{else}bi-star{/if} me-1" data-value="{$smarty.section.star.index}"></i>
{/section}
{* 2. Рейтинг комментария *}
{if $comment_rating_type != 2}
<div class="comment-rating-container d-flex align-items-center py-1 px-2 bg-light rounded border" data-id="{$c.Id}">
{if $comment_rating_type == 1}
<div class="comment-like text-danger" style="cursor: pointer;">
<i class="star-item bi bi-heart-fill me-1" data-value="5"></i>
<span class="fw-bold small">{$c.rating_count|default:0}</span>
</div>
{if isset($c.rating_count)}<span class="text-muted small">({$c.rating_count})</span>{/if}
</div>
{/if}
</div>
{/if}
{else}
<div class="comment-stars text-warning" style="cursor: pointer;">
{assign var="avg_rating" value=0}
{if isset($c.rating_count) && $c.rating_count > 0}
{math equation="round(x / y)" x=$c.rating_sum y=$c.rating_count assign="avg_rating"}
{/if}
<div class="d-flex align-items-center me-1">
{section name=star start=1 loop=6}
<i class="star-item bi {if $smarty.section.star.index <= $avg_rating}bi-star-fill{else}bi-star{/if} me-1" data-value="{$smarty.section.star.index}"></i>
{/section}
</div>
{if isset($c.rating_count)}<span class="text-muted small">({$c.rating_count})</span>{/if}
</div>
{/if}
</div>
{/if}
{* 3. Таймер редактирования *}
{if $c.can_edit && isset($c.edit_time_left) && $c.edit_time_left > 0}
<div class="text-muted small d-flex align-items-center" id="timer_container_{$c.Id}">
<i class="bi bi-hourglass-split text-primary me-1"></i>
<span id="timer_{$c.Id}" data-left="{$c.edit_time_left}" class="timer-count fw-bold">
{math equation="floor(x/60)" x=$c.edit_time_left}:{if ($c.edit_time_left%60) < 10}0{/if}{math equation="x%60" x=$c.edit_time_left}
</span>
</div>
{* 3. Таймер редактирования *}
{if $c.can_edit && isset($c.edit_time_left) && $c.edit_time_left > 0}
<div class="text-muted small d-flex align-items-center" id="timer_container_{$c.Id}">
<i class="bi bi-hourglass-split text-primary me-1"></i>
<span id="timer_{$c.Id}" data-left="{$c.edit_time_left}" class="timer-count fw-bold">
{math equation="floor(x/60)" x=$c.edit_time_left}:{if ($c.edit_time_left%60) < 10}0{/if}{math equation="x%60" x=$c.edit_time_left}
</span>
</div>
{/if}
{/if}
</div>
{* КНОПКИ ДЕЙСТВИЙ *}
<div class="actions-buttons d-flex align-items-center gap-1">
{if ($cancomment==1 && $closed!=1) || $smarty.const.UGROUP==1}
{if $comment_allow_self_answer == 1 || !(isset($c.is_my_own) && $c.is_my_own)}
<a class="btn btn-sm btn-link text-primary text-decoration-none mod_comment_answer px-2" href="javascript:void(0);" data-id="{$c.Id}">
<i class="bi bi-reply-fill me-1"></i> <span class="d-none d-sm-inline">{#COMMENT_ANSWER_LINK#}</span>
{if !$is_deleted}
{if ($cancomment==1 && $closed!=1) || $smarty.const.UGROUP==1}
{if $comment_allow_self_answer == 1 || !(isset($c.is_my_own) && $c.is_my_own)}
<a class="btn btn-sm btn-link text-primary text-decoration-none mod_comment_answer px-2" href="javascript:void(0);" data-id="{$c.Id}">
<i class="bi bi-reply-fill me-1"></i> <span class="d-none d-sm-inline">{#COMMENT_ANSWER_LINK#}</span>
</a>
{/if}
{/if}
<span id="controls_{$c.Id}" class="d-flex align-items-center gap-1">
{if $c.can_edit}
<a class="btn btn-sm btn-link text-warning mod_comment_edit px-2" title="{#COMMENT_EDIT_LINK#}" href="javascript:void(0);" data-id="{$c.Id}"><i class="bi bi-pencil-square"></i></a>
<a class="btn btn-sm btn-link text-danger mod_comment_delete px-2" title="{#COMMENT_DELETE_LINK#}" href="javascript:void(0);" data-id="{$c.Id}"><i class="bi bi-trash"></i></a>
{/if}
</span>
{if $smarty.const.UGROUP==1}
<a class="btn btn-sm btn-link {if $c.comment_status==1}text-success{else}text-danger{/if} mod_comment_toggle px-2"
href="javascript:void(0);"
data-id="{$c.Id}"
title="{if $c.comment_status==1}{#COMMENT_ICON_HIDE#}{else}{#COMMENT_ICON_SHOW#}{/if}">
<i class="bi bi-{if $c.comment_status==1}eye{else}eye-slash{/if}"></i>
</a>
{/if}
{/if}
{* Контейнер для кнопок, которые скрываются таймером *}
<span id="controls_{$c.Id}" class="d-flex align-items-center gap-1">
{if $c.can_edit}
<a class="btn btn-sm btn-link text-warning mod_comment_edit px-2" title="{#COMMENT_EDIT_LINK#}" href="javascript:void(0);" data-id="{$c.Id}"><i class="bi bi-pencil-square"></i></a>
<a class="btn btn-sm btn-link text-danger mod_comment_delete px-2" title="{#COMMENT_DELETE_LINK#}" href="javascript:void(0);" data-id="{$c.Id}"><i class="bi bi-trash"></i></a>
{/if}
</span>
{if $smarty.const.UGROUP==1}
<a class="btn btn-sm btn-link {if $c.comment_status==1}text-success{else}text-danger{/if} mod_comment_toggle px-2"
href="javascript:void(0);"
data-id="{$c.Id}"
title="{if $c.comment_status==1}{#COMMENT_ICON_HIDE#}{else}{#COMMENT_ICON_SHOW#}{/if}">
<i class="bi bi-{if $c.comment_status==1}eye{else}eye-slash{/if}"></i>
</a>
{/if}
</div>
</div>
</div>
@@ -228,16 +251,15 @@
<span id="end{$c.Id}"></span>
{if isset($comments) && isset($comments[$c.Id])}
<div class="mt-2">
{* передаем parentId=$c.Id для следующего уровня вложенности *}
{include file="$subtpl" subcomments=$comments[$c.Id] sub=1 parentId=$c.Id}
</div>
{/if}
{if isset($comments) && isset($comments[$c.Id])}
<div class="mt-2">
{include file="$subtpl" subcomments=$comments[$c.Id] sub=1 parentId=$c.Id}
</div>
{/if}
</div>
{* --- ВСТАВЛЯЕМ КНОПКУ AJAX ОТВЕТОВ --- *}
{* --- ВСТАВЛЯЕМ КНОПКУ AJAX ОТВЕТОВ --- *}
{if $smarty.foreach.sub_loop.last && isset($more_counts[$parentId])}
<div id="ajax_loader_{$parentId}" class="ms-4 mb-3">
<button class="btn btn-sm btn-outline-primary" onclick="loadMoreReplies('{$parentId}')">