Создание отзывов на сайте с автоматической публикацией по кнопке в письме

В этой статье мы разберём, как реализовать систему отзывов на сайте, построенном на MODX, с возможностью для клиентов отправлять отзывы с прикреплёнными фотографиями. Отзывы сохраняются в статусе “неопубликовано” и публикуются только после того, как администратор нажмёт на кнопку “Опубликовать” в письме-уведомлении. Этот подход позволяет модерировать контент, сохраняя удобство публикации. Мы будем использовать пакет SendIt для обработки форм и отправки писем, а я подробно объясню каждый шаг и предоставлю разъяснения к коду. Для этого нам нужно

Шаг 1: Создание формы для отправки отзывов

Первый шаг — создать удобную форму, через которую клиенты смогут отправлять свои отзывы. Форма включает поля для имени, электронной почты, текста отзыва и возможность загрузки фотографий. Мы используем пресет form_with_file из SendIt для обработки текстовых данных и файлов.

{set $fullurl = $_modx->resource.id | url : ['scheme' => 'full']}
<form 
    data-si-event="submit" 
    data-si-form="Review" 
    data-si-preset="form_with_file" 
    enctype="multipart/form-data"
    id="form2">
    <input type="text" name="name" placeholder="Ваше имя" required />
    <input type="email" name="email" placeholder="Электронная почта" required />        
    <textarea class="c-form_textarea" id="input-mes" name="message" placeholder="Ваш отзыв" required></textarea>
    <div class="c-form_item dragdrop" data-fu-wrap data-si-preset="upload_drop_file" data-si-nosave>
        <div data-fu-progress=""></div>
        <input type="hidden" name="filelist" data-fu-list>
        <label data-fu-dropzone>
            <input type="file" name="files" data-fu-field multiple class="v_hidden">
            <span class="drag_title">Перетащите ваши фотографии</span>
        </label>
        <template data-fu-tpl>
            <button type="button" data-fu-path="$path" class="drop-btn" style="background-image:url($path)"></button>
        </template>
    </div>  
    <button class="fasong" type="submit">Отправить</button>
    <div id="sendit-spinner" style="display: none;">⏳ Отправка...</div>        
</form>

<script type="text/javascript" defer>
    document.addEventListener("DOMContentLoaded", function() {
        const status = document.getElementById("sendit-spinner");
        const button = document.querySelector("button.fasong");
    
        document.addEventListener("si:send:before", function () {
            status.style.display = "block";
            button.disabled = true;
        });
      
        document.addEventListener("si:send:finish", (e) => {
            const { action, target, result, headers, Sending } = e.detail;
            status.style.display = "none";
            button.disabled = false;
        });
    });
</script>Code language: HTML, XML (xml)

Объяснение

  • Структура формы: Форма использует пресет form_with_file от SendIt для обработки текстовых полей и загрузки файлов. Атрибуты data-si-* настраивают SendIt для обработки отправки формы.
  • Поля формы: Включают name (имя), email (электронная почта), message (текст отзыва) и поле для загрузки файлов с поддержкой drag-and-drop через пресет upload_drop_file.
  • JavaScript: Обрабатывает события SendIt (si:send:before и si:send:finish), показывая индикатор загрузки и отключая кнопку отправки во время обработки, что улучшает пользовательский опыт.
  • Интеграция с MODX: Сниппет {set $fullurl …} генерирует полный URL текущей страницы, что полезно для обработки формы или редиректов.

Шаг 2: Настройка SendIt с пользовательским хуком

Для обработки данных формы и создания нового ресурса отзыва добавим пользовательский хук SaveToReviews в пресеты SendIt. Этот хук обрабатывает данные формы, сохраняет отзыв как ресурс MODX, сжимает загруженные изображения и генерирует уникальный токен для публикации через email.

Добавление в пресеты SendIt

В SendIt добавьте хук SaveToReviews в пресет form_with_file (или создайте новый пресет). Это обеспечит вызов хука при отправке формы. Про пресеты расписано подробно на официальной документации SendIt

<?php
function resizeImage($sourcePath, $destinationPath, $maxWidth = 1200, $maxHeight = 1200, $quality = 85) {
    $info = getimagesize($sourcePath);
    if (!$info) return false;

    list($width, $height) = $info;
    $mime = $info['mime'];
    $scale = min($maxWidth / $width, $maxHeight / $height, 1);
    $newWidth = (int)($width * $scale);
    $newHeight = (int)($height * $scale);
    $dst = imagecreatetruecolor($newWidth, $newHeight);

    switch ($mime) {
        case 'image/jpeg':
            $src = imagecreatefromjpeg($sourcePath);
            break;
        case 'image/png':
            $src = imagecreatefrompng($sourcePath);
            imagealphablending($dst, false);
            imagesavealpha($dst, true);
            break;
        case 'image/gif':
            $src = imagecreatefromgif($sourcePath);
            break;
        default:
            return false;
    }

    imagecopyresampled($dst, $src, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);

    switch ($mime) {
        case 'image/jpeg':
            imagejpeg($dst, $destinationPath, $quality);
            break;
        case 'image/png':
            imagepng($dst, $destinationPath, 6);
            break;
        case 'image/gif':
            imagegif($dst, $destinationPath);
            break;
    }

    imagedestroy($src);
    imagedestroy($dst);
    return true;
}

$name = strip_tags($hook->getValue('name'));
$email = strip_tags($hook->getValue('email'));
$message = strip_tags($hook->getValue('message'));
$filelist = $hook->getValue('filelist');

$randomNumber = random_int(10000, 99999);
$alias = preg_replace('/[^a-zA-Z0-9]+/', '-', $name . $randomNumber);
$alias = strtolower(trim($alias, '-'));

if ($modx->getCount('modResource', ['parent' => 32, 'alias' => $alias]) == 0) {
    $newReview = $modx->newObject('modResource');
    $token = bin2hex(random_bytes(16));

    $newReview->fromArray([
        'pagetitle' => 'Отзыв от ' . $name,
        'longtitle' => $name,
        'content' => $message,
        'parent' => 32,
        'template' => 11,
        'published' => 0,
        'publishedon' => time(),
        'hidemenu' => 1,
        'alias' => $alias,
        'show_in_tree' => 0,
    ]);

    if ($newReview->save()) {
        $publishUrl = $modx->makeUrl(59, '', [
            'action' => 'publish_review',
            'review_id' => $newReview->get('id'),
            'token' => $token
        ], 'full');

        $fields = $hook->getValues();
        $fields['publish_url'] = $publishUrl;
        $hook->setValues($fields);
        $modx->setPlaceholder('publish_url', $publishUrl);

        $migxData = [];
        if (!empty($filelist)) {
            $basePath = $modx->getOption('base_path') . 'assets/images/portfolio/';
            $baseUrl = $modx->getOption('base_url') . 'assets/images/portfolio/';
            if (!file_exists($basePath)) {
                mkdir($basePath, 0755, true);
            }
            $sessionId = session_id();
            $sourceDir = $modx->getOption('base_path') . 'assets/components/sendit/uploaded_files/' . $sessionId . '/';
            $files = explode(',', $filelist);

            foreach ($files as $fileName) {
                $sourcePath = $sourceDir . $fileName;
                $destinationPath = $basePath . $randomNumber . '_' . $fileName;
                if (file_exists($sourcePath)) {
                    if (resizeImage($sourcePath, $destinationPath, 1200, 1200)) {
                        $migxData[] = [
                            'image' => 'assets/images/portfolio/' . $randomNumber . '_' . $fileName,
                            'MIGX_id' => count($migxData) + 1,
                        ];
                    } else {
                        $modx->log(modX::LOG_LEVEL_ERROR, "Не удалось сжать файл: {$sourcePath}");
                    }
                }
            }

            if (!empty($migxData)) {
                $newReview->setTVValue('ReviewsImgMigx', json_encode($migxData));
            } else {
                $modx->log(modX::LOG_LEVEL_ERROR, "Нет данных для MIGX: filelist={$filelist}");
            }
        }

        $newReview->setTVValue('ReviewToken', $token);
        $modx->log(modX::LOG_LEVEL_INFO, "Создан отзыв: {$name}");
    } else {
        $modx->log(modX::LOG_LEVEL_ERROR, "Не удалось сохранить отзыв: {$name}");
    }
    unset($newReview);
} else {
    $modx->log(modX::LOG_LEVEL_ERROR, "Отзыв уже существует: {$name}");
}

return true;Code language: HTML, XML (xml)

Объяснение

  • Сжатие изображений: Функция resizeImage сжимает и изменяет размер загруженных изображений до максимум 1200×1200 пикселей, поддерживая форматы JPEG, PNG и GIF.
  • Данные формы: Очищает входные данные (name, email, message) с помощью strip_tags для защиты от XSS-атак.
  • Уникальный alias: Генерирует уникальный alias для ресурса отзыва, используя имя клиента и случайное число.
  • Создание ресурса: Создаёт новый ресурс MODX под родительским ID 32 с шаблоном ID 11, изначально в статусе “неопубликовано” (published => 0).
  • Обработка файлов: Переносит загруженные файлы из временной директории SendIt в постоянное хранилище, сжимает их и сохраняет пути в поле MIGX TV (ReviewsImgMigx) для отображения.
  • Генерация токена: Создаёт уникальный 32-символьный токен для безопасной публикации через email.
  • URL для публикации: Формирует URL с параметрами action, review_id и token, передавая его в письмо через плейсхолдер publish_url.

Шаг 3: Создание шаблона письма

При отправке отзыва администратору отправляется письмо с кнопкой “Опубликовать”. Мы создадим чанк emailReview.tpl для форматирования этого письма.

{set $fields = $fields | replace: '"' : '"' | fromJSON}
{set $fieldsAliases = $fieldsAliases | replace: '"' : '"' | fromJSON}

<h3>Получен новый отзыв с сайта test.com</h3>
<p>Подробности:</p>

<style>
#table2024 {
    width: 100%;
    border-collapse: collapse;
}
#table2024 tr:nth-child(even) {
    background-color: #f2f2f2;
}
#table2024 td {
    padding: 12px;
    border: 1px solid #ddd;
    text-align: left;
    box-sizing: border-box;
}
</style>

<table id="table2024">
    <tbody>
    {foreach $fields as $k => $v}
        {if $v}
            <tr>
                <td>{$fieldsAliases[$k] ?: $k}:</td>
                <td><strong>{$v}</strong></td>
            </tr>
        {/if}
    {/foreach}
    </tbody>
</table>    

<p><strong>Требуется действие:</strong> Чтобы опубликовать этот отзыв, нажмите на кнопку ниже.</p>

<p>
    <a href="{$publish_url}" target="_blank" style="color:#fff;font-weight:600;padding:10px 20px;border-radius:5px;background:#007bff;text-decoration:none">Опубликовать отзыв</a>
</p>
<p><em>Примечание:</em> Чтобы привязать этот отзыв к конкретному проекту, обновите его в админ-панели.</p>Code language: HTML, XML (xml)

Объяснение

  • Динамические поля: Цикл {foreach} отображает все поля формы в стилизованной таблице, используя $fieldsAliases для удобных меток.
  • Стилизация: Чистый и отзывчивый дизайн таблицы с чередующимися цветами строк для удобства чтения.
  • Кнопка публикации: Стилизованная ссылка <a> ведёт на плейсхолдер publish_url, позволяя опубликовать отзыв одним кликом.
  • Примечание: Указывает администратору, что для привязки отзыва к проекту нужно обновить данные в админ-панели MODX.

Настройте SendIt, указав параметр emailTpl как emailReview.tpl в пресете.

Шаг 4: Обработка действия публикации

Для обработки клика по кнопке “Опубликовать” из письма создадим сниппет PublishReview. Его разместим на скрытом ресурсе MODX (например, ID 59) с пользовательским шаблоном.

<?php
$action = $modx->getOption('action', $_GET, '');
$reviewId = (int)$modx->getOption('review_id', $_GET, 0);
$token = $modx->getOption('token', $_GET, '');

if ($action !== 'publish_review' || $reviewId <= 0 || empty($token)) {
    $modx->log(modX::LOG_LEVEL_ERROR, "Некорректные параметры для публикации: action={$action}, review_id={$reviewId}, token={$token}");
    return 'Ошибка: некорректные параметры.';
}

$review = $modx->getObject('modResource', $reviewId);
if (!$review) {
    $modx->log(modX::LOG_LEVEL_ERROR, "Отзыв с ID {$reviewId} не найден.");
    return 'Ошибка: отзыв не найден.';
}

if ($review->get('published')) {
    return 'Отзыв уже опубликован.';
}

$storedToken = $review->getTVValue('ReviewToken');
if ($storedToken !== $token) {
    $modx->log(modX::LOG_LEVEL_ERROR, "Неверный токен для отзыва ID {$reviewId}. Получен: {$token}, Ожидался: {$storedToken}");
    return 'Ошибка: неверный токен для отзыва ID ' . $reviewId . '. Получен: ' . $token . ', Ожидался: ' . $storedToken;
}

$review->set('published', 1);
$review->set('publishedon', time());
$review->setTVValue('ReviewToken', '');

if ($review->save()) {
    $modx->log(modX::LOG_LEVEL_INFO, "Отзыв ID {$reviewId} успешно опубликован.");
    return 'Отзыв успешно опубликован!';
} else {
    $modx->log(modX::LOG_LEVEL_ERROR, "Не удалось опубликовать отзыв ID {$reviewId}.");
    return 'Ошибка при публикации отзыва.';
}Code language: HTML, XML (xml)

Объяснение

  • Проверка параметров: Проверяет наличие корректных параметров action, review_id и token в URL.
  • Поиск ресурса: Находит ресурс отзыва по ID.
  • Проверка токена: Убеждается, что предоставленный токен совпадает с сохранённым значением ReviewToken в TV-поле.
  • Публикация: Устанавливает published в 1, обновляет publishedon, очищает токен для безопасности и сохраняет ресурс.
  • Обработка ошибок: Логирует ошибки и возвращает понятные сообщения для некорректных случаев.

Инструкции по настройке

  1. Создайте скрытый ресурс MODX (например, ID 59) с минимальным шаблоном.
  2. Разместите сниппет PublishReview на этом ресурсе с помощью {‘!PublishReview’ | snippet}.
  3. Убедитесь, что ресурс доступен, но не отображается в меню (hidemenu => 1).

Заключение

Эта система обеспечивает надёжный механизм для работы с отзывами на сайте MODX, сочетая удобную форму отправки, безопасную обработку файлов и модерируемую публикацию через email. Использование SendIt, пользовательских хуков и системы ресурсов MODX гарантирует удобство как для клиентов, так и для администраторов. Токены добавляют слой безопасности, предотвращая несанкционированную публикацию, а кнопка в письме упрощает процесс одобрения.

Понравилась статья? Поделиться с друзьями:
Добавить комментарий

;-) :| :x :twisted: :smile: :shock: :sad: :roll: :razz: :oops: :o :mrgreen: :lol: :idea: :grin: :evil: :cry: :cool: :arrow: :???: :?: :!: