Единый форум поддержки

Информация о пользователе

Привет, Гость! Войдите или зарегистрируйтесь.


Вы здесь » Единый форум поддержки » Новые возможности форумов » Запросы по скриптам #11


Запросы по скриптам #11

Сообщений 1741 страница 1760 из 1764

1

В этой теме просим о создании различных скриптов

Здесь вы можете оставить запрос на разработку нового скрипта для форума. Пожалуйста, внимательно ознакомьтесь с правилами темы. Просьба быть внимательным: эта тема касается только вопросов о создании новых скриптов!
Вопросы по оформлению форума и прочим CSS-кодам, просьба, задавать в темах Общие вопросы от новичков (63) #3 и Общие вопросы по оформлению (65) #2

Информируем о следующем:

  • Участники форума оказывают помощь в написании скриптов исключительно по собственному желанию.

  • Администрация форума не может гарантировать исполнение и корректность каждого запроса.

  • Пожалуйста, уважайте чужое время и усилия других пользователей, старайтесь писать грамотно и доходчиво.

  • Будьте взаимовежливы: Грубое или требовательное отношение к участникам форума неприемлемо и может привести к отказу в помощи на всём форуме.

  • Все вопросы, не касающиеся запросов скриптов, будут удаляться!

Сформулируйте свой запрос максимально подробно, чтобы повысить вероятность отклика:

  • Опишите желаемую функциональность: Что конкретно должен делать скрипт?

  • Укажите конечный результат: Что вы хотите получить в итоге? Где и как будет использоваться скрипт?

  • Если есть примеры, покажите: Укажите ссылки на схожий функционал скрипта или нарисуйте макет (на скриншоте), чего именно вы ожидаете получить - всё это значительно упростит задачу.

Если на ваш запрос долго нет ответа, пожалуйста, воздержитесь от повторных сообщений или обвинений. Возможно, задача оказалась слишком сложной или трудозатратой по времени, будьте готовы к тому, что участники могут предложить свои услуги на платной основе. Обсуждение условий и оплаты происходит между вами и участником (исполнителем) в личных сообщениях или иных средствах связи (но не в теме!). Администрация форума не несет никакой ответственности за подобные договоренности и их выполнение.

Если скрипт был написан на безвозмездной основе, автор может разместить его в общем каталоге скриптов для других пользователей.

Предыдущая тема - Запросы по скриптам[10]

Инструменты для поиска и редактирования стиля (скриптов).

Как с помощью браузера можно определить элемент дизайна.

Каталог скриптов/CSS

Полезные скрипты, необходимые темы для новичков, а также ссылки на сайты рассказывающие что такое НТМЛ и CSS.

Типовые Вопросы (ЧаВо)

Ответы на часто задаваемые вопросы.

Как правильно задавать вопросы. В чём разница между стилем и скриптом.

Плюс к названию темы еще и Памятка.

+1

1741

Амираль

Очень интересно, но, как всегда, ничего не понятно - какая-то кнопка в итоге должна появится или что?)
И как найти вас в телеграме?:)

0

1742

Амираль написал(а):

буду ультра благодарна тому, кто перевезет пост в скрипты от пользаков, idk как я уже второй раз вместо той темы закидываю сюда

Сделано специально, чтобы в скрипты от пользователей попадали только проверенные коды, а не любой публикуемый там пост (зачастую это вопросы, которые должны быть по идее адресованы в эту тему, или непроверенные/дублирующиеся коды). Перенёс в нужную тему.

+1

1743

Merlin777 написал(а):

⚡Мгновенные "Быстроплюсы" и ⚡Мгновенные "Сказали спасибо"

Здравствуйте! Поставил этот скрипт - всё работает прекрасно. Но. Почему-то в личной переписке в каждом письме появились "Сказали спасибо" - и  лайки от всё время разных форумчан (не одни и те же). Причём эти лайки есть абсолютно во всех письмах. И даже есть в тех письмах, которые были написаны, когда на форуме вообще ни одного скрипта не было установлено.

Скриншот

Отредактировано Виплич (Пн, 26 Янв 2026 13:18:47)

0

1744

Виплич
По идее нужно ограничить выполнение скрипта только для топиков, ID сообщений в топиках могут пересекаться с таковыми в ЛС, что в данном случае будет некорректно. Что-то типа такого, думаю (добавка выделена красным):

function scheduleUpdate(scope = document) {
    if (!$('#pun-viewtopic').length) return;

+1

1745

@Виплич

Вот версия (CSS + JavaScript) с проверкой работы на страницах топика:

Посмотреть код
Код:
<style>
/*Быстроплюсы для Топика!*/
div .post-rating p a {
    text-align: center;
    outline: 1px solid transparent;
    font-weight: 700;
    background: url(https://upforme.ru/uploads/001a/f0/7d/2/466540.png  ) no-repeat center;
     background-size:36px auto;
     position:relative;
    z-index:100;
    width: 36px;
    height: 36px;
    text-align:center;
    font-size: 12px !important;
    display: inline-block;
    vertical-align: middle;
    line-height: 34px;
    letter-spacing: -.1px;
}
.noNull:before{content: "+";
    display: inline-block;
    margin-left: -1px;
    font-size: 9px;
    letter-spacing: 0!important;
}
.punbb .post-rating p {
   padding: 0 !important;
}
</style>

<script>
    'use strict';
 
// --- ОБЩИЕ ВСПОМОГАТЕЛЬНЫЕ ФУНКЦИИ ---
 
// Получает ID постов из указанного DOM-контекста
function getPostIds(scope = document) {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return [];
    
    const postElements = $(scope).find('div.post');
    return postElements.map(function() {
        const id = $(this).attr('id');
        if (id && id.startsWith('p')) {
            return id.slice(1);
        }
    }).get();
}
 
// --- ЧАСТЬ 1: БЫСТРОПЛЮСЫ ---
 
// Отслеживание уже обработанных постов (для избежания дублирования)
const processedPostsForPlus = new Set();
 
// Обновляет отображение цифры рейтинга и управляет классом noNull
function setDigit(th) {
    var d = parseInt(th.innerHTML);
    if (d > 0) $(th).addClass('noNull');
    else $(th).removeClass('noNull');
    th.innerHTML = d;
}
 
// Сохранение оригинального alert для восстановления после голосования
var Busy = window.alert;
 
// Обработка клика по кнопке "плюса": отправка запроса, обновление рейтинга и мини-профиля
function setPlus(sel) {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    window.alert = null;
    var a = sel.prop('href');
    var pid = a.match(/\?id=(\d+)/)[1];
 
    var uid = sel.parents('.post').find('.pl-email a[href*="profile.php?"]').prop('href');
    if (uid) uid = uid.match(/\?id=(\d+)$/)[1];
    var v = a.match(/&v=(\d+)/)[1] == 0 ? -1 : 1;
    $('#post-' + pid + '-vote').hide();
 
    $.get(a + '&format=json', function(data) {
        if (data.error && data.error.message) {
            // Ошибка обрабатывается без уведомлений — только внутренняя логика
        } else if (data.delta) {
            if(data.response !== undefined) {
                $('#p' + pid + ' .post-rating a').text(data.response);
                setDigit($('#p' + pid + ' .post-rating a')[0]);
            } else {
                var oldRating = parseInt($('#p' + pid + ' .post-rating a').text()) || 0;
                var newRating = oldRating + data.delta;
                $('#p' + pid + ' .post-rating a').text(newRating);
                setDigit($('#p' + pid + ' .post-rating a')[0]);
            }
 
            // Обновление мини-профиля автора и текущего пользователя
            var $res = $('.pl-email a[href$="profile.php?id=' + uid + '"]').parents('.post').find('.pa-respect');
            var $pos = $('.pl-email a[href$="profile.php?id=' + UserID + '"]').parents('.post').find('.pa-positive');
 
            function replaceRating(sel, v, revert) {
                var html = $(sel).html(),
                    delta = v;
                if (revert) delta = delta > 0 ? -1 : 1;
                if (v > 0) {
                    html = html.replace(/\[\+(\d+)\//g, function(str, p1) {
                        return '[+' + (parseInt(p1) + delta) + '/';
                    });
                } else {
                    html = html.replace(/\/-(\d+)\]/g, function(str, p1) {
                        return '/-' + (parseInt(p1) - delta) + ']';
                    });
                }
                $(sel).html(html);
            }
            if ($res.html().indexOf('[') != -1) {
                $res.each(function() { replaceRating(this, v); });
                $pos.each(function() { replaceRating(this, v); });
                if (Math.abs(data.delta) == 2) {
                    v = v > 0 ? -1 : 1;
                    $res.each(function() { replaceRating(this, v, 1); });
                    $pos.each(function() { replaceRating(this, v, 1); });
                }
            } else {
                var d0 = $res.find('span:not(.fld-name)').html(),
                    p0 = $pos.find('span:not(.fld-name)').html();
                var d1 = parseInt(d0) + v;
                if (p0) {
                    var p1 = parseInt(p0) + v;
                }
                if (d1 && d1 > 0) {
                    d1 = '+' + d1;
                }
                if (p1 && p1 > 0) {
                    p1 = '+' + p1;
                }
                $res.find('span:not(.fld-name)').html(d1);
                if (p0) $pos.find('span:not(.fld-name)').html(p1);
            }
 
            // Генерация события для обновления списка "Спасибо" при лайке
            if(v === 1) {
                document.dispatchEvent(new CustomEvent('vote:happened', {
                    detail: { pid: pid, type: 'thank' }
                }));
            }
        }
        setTimeout(function() {window.alert = Busy;},1300);
    })
    .fail(function(xhr, status, error) {
        setTimeout(function() {window.alert = Busy;},1300);
    });
}
 
// Обёртка для вызова setPlus из onclick
window.BR = function(th) {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    var lnk = $(th).parents('.post-box').find('.post-vote>p>a');
    setPlus(lnk);
};
 
// Скрывает все вновь добавленные элементы .post-vote
function hideAllNewPostVotes(scope = document) {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    $(scope).find('.post-vote[id^="post-"]').each(function() {
        var postId = this.id.replace('post-', '');
        var postRatingLink = $(`.post#p${postId} .post-rating p a`);
        if(postRatingLink.length) {
             $(this).hide();
        }
    });
}
 
// Назначает обработчики для кнопок "плюса" в указанной области
function applyPlusHandlers(scope = document) {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    $(scope).find('.post .post-rating p a').each(function() {
        if (!this.hasAttribute('data-plus-handler')) {
             this.setAttribute("onclick", "BR(this)");
             setDigit(this);
 
             var postVoteLinkSelector = $(this).parents('.post').attr('id');
             if(postVoteLinkSelector) {
                 var pid = postVoteLinkSelector.slice(1);
                 var postVoteLink = $('#post-' + pid + '-vote');
                 if(postVoteLink.length) {
                      postVoteLink.hide();
                 }
             }
             this.setAttribute('data-plus-handler', 'true');
        }
    });
    hideAllNewPostVotes(scope);
}
 
// --- ЧАСТЬ 2: СПИСОК ПОБЛАГОДАРИВШИХ (API-ЛОГИКА) ---
 
// Отслеживание постов, для которых уже запрашивались "спасибо"
const processedPostsForThanks = new Set();
 
// Отслеживание ID постов, для которых выполняется запрос (для дедупликации)
const pendingUpdates = new Set();
 
// Вставка списка благодаривших в пост
function insertThanksList(postId, thankersListHtml) {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    if (!thankersListHtml) {
        return;
    }
 
    var postBox = $('.post#p' + postId + ' .post-box');
    if (postBox.length && postBox.find('.postVoters').length === 0) {
        var html = '<div class="postVoters"><strong><i><font color ="#13355e">Сказали спасибо</font></i>:</strong> ' + thankersListHtml + '</div>';
        postBox.append(html);
        processedPostsForThanks.add(postId);
    } else {
        if(postBox.length === 0) {
            var postElement = $('.post#p' + postId);
            if(postElement.length && postElement.find('.postVoters').length === 0) {
                var html = '<div class="postVoters"><strong><i><font color ="#13355e">Сказали спасибо</font></i>:</strong> ' + thankersListHtml + '</div>';
                postElement.append(html);
                processedPostsForThanks.add(postId);
            }
        } else {
            postBox.find('.postVoters').html('<strong><i><font color ="#13355e">Сказали спасибо</font></i>:</strong> ' + thankersListHtml);
        }
    }
}
 
// Обработка ответа API: группировка и вставка благодаривших
function processVotes(data) {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    if (!data || !data.response) {
        const postIdsFromRequest = currentFetchRequestIds || [];
        postIdsFromRequest.forEach(id => processedPostsForThanks.add(id));
        currentFetchRequestIds = null;
        return;
    }
 
    const votesByPost = {};
    data.response.forEach(item => {
        const postId = item.post_id;
        if (item.votes && Array.isArray(item.votes)) {
            item.votes.forEach(vote => {
                if (vote.value === '1') {
                    if (!votesByPost[postId]) {
                        votesByPost[postId] = [];
                    }
                    votesByPost[postId].push(vote);
                }
            });
        }
    });
 
    Object.keys(votesByPost).forEach(postId => {
        const votesForThisPost = votesByPost[postId];
        let thankersHtml = '';
        votesForThisPost.forEach((vote, index) => {
            thankersHtml += '<a href="/profile.php?id=' + vote.user_id + '">' + vote.username + '</a>';
            if (index < votesForThisPost.length - 1) {
                thankersHtml += ', ';
            }
        });
        insertThanksList(postId, thankersHtml);
    });
 
    Object.keys(votesByPost).forEach(id => processedPostsForThanks.add(id));
    data.response.forEach(item => {
        if (!processedPostsForThanks.has(item.post_id)) {
            processedPostsForThanks.add(item.post_id);
        }
    });
}
 
// Выполнение API-запроса для получения списка благодаривших
let currentFetchRequestIds = null;
 
function fetchVotesForPosts(postIds) {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    if (!postIds || postIds.length === 0) {
        postIds.forEach(id => pendingUpdates.delete(id));
        return;
    }
 
    const apiParams = {
        method: 'post.getVotesByPosts',
        post_id: postIds.join(','),
        fields: 'post_id,user_id,username,value,datetime',
        sort_dir: 'desc'
    };
 
    currentFetchRequestIds = [...postIds];
 
    return $.get('/api.php', apiParams, function(data) {
        processVotes(data);
    }, 'json')
    .fail(function(xhr, status, error) {
        postIds.forEach(id => processedPostsForThanks.add(id));
        currentFetchRequestIds = null;
    });
}
 
// Планирование обновления списка "спасибо" для новых постов
function scheduleUpdate(scope = document) {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    requestAnimationFrame(() => {
        const newPostIds = getPostIds(scope).filter(id => !processedPostsForThanks.has(id));
        if (newPostIds.length > 0) {
            const idsToRequest = newPostIds.filter(id => !pendingUpdates.has(id));
            if (idsToRequest.length > 0) {
                idsToRequest.forEach(id => pendingUpdates.add(id));
                fetchVotesForPosts(idsToRequest).always(() => {
                    idsToRequest.forEach(id => pendingUpdates.delete(id));
                });
            }
        }
    });
}
 
// Объединённая функция резервного обновления (раз в секунду)
function combinedBackupUpdate() {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    applyPlusHandlers(document);
    scheduleUpdate(document);
}
 
// --- ИНИЦИАЛИЗАЦИЯ И ПОДПИСКИ НА СОБЫТИЯ ---
 
// Инициализация при загрузке DOM
$(function() {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    $('.post .post-rating p a').each(function() {
        this.setAttribute("onclick", "BR(this)");
        setDigit(this);
        var postVoteLinkSelector = $(this).parents('.post').attr('id');
        if(postVoteLinkSelector) {
            var pid = postVoteLinkSelector.slice(1);
            var postVoteLink = $('#post-' + pid + '-vote');
            if(postVoteLink.length && $(this).hasClass('noNull')) {
                postVoteLink.hide();
            }
        }
        this.setAttribute('data-plus-handler', 'true');
    });
 
    scheduleUpdate(document);
});
 
// Подписка на событие добавления нового поста (например, через AJAX)
document.addEventListener('pun_post', function(e) {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    var scope = e.detail?.post || document.body;
    applyPlusHandlers(scope);
    scheduleUpdate(scope);
});
 
// Подписка на собственное событие после успешного лайка
document.addEventListener('vote:happened', function(e) {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    if (e.detail && e.detail.pid && e.detail.type === 'thank') {
        var pid = e.detail.pid;
        var specificPostScope = $('#p' + pid);
        if(specificPostScope.length) {
            if (!pendingUpdates.has(pid)) {
                pendingUpdates.add(pid);
                fetchVotesForPosts([pid]).always(() => {
                    pendingUpdates.delete(pid);
                });
            }
        }
    }
});
 
// Подписка на глобальное AJAX-событие для отслеживания лайков от любых пользователей
$(document).on('ajaxSuccess', function(e, xhr, data) {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    if (data && data.url && /relation\.php.*[&?]v=(\d+)/.test(data.url)) {
        const match = data.url.match(/id=(\d+)/);
        const pid = match ? match[1] : null;
        const voteType = data.url.match(/v=(\d+)/)?.[1];
 
        if (pid && voteType === '1') {
            if (!pendingUpdates.has(pid)) {
                pendingUpdates.add(pid);
                fetchVotesForPosts([pid]).always(() => {
                    pendingUpdates.delete(pid);
                });
            }
        }
    }
});
 
// Наблюдатель за изменениями DOM для динамических постов
const observer = new MutationObserver(function(mutationsList) {
    // Работаем только на странице топика
    if (!$('#pun-viewtopic').length) return;
    
    let shouldUpdate = false;
    let relevantScope = document.body;
 
    for (let mutation of mutationsList) {
        if (mutation.type === 'childList') {
            for (let node of mutation.addedNodes) {
                if (node.nodeType === 1) {
                    if (node.classList && node.classList.contains('post')) {
                        relevantScope = node;
                        shouldUpdate = true;
                        break;
                    } else if (node.querySelector && node.querySelector('div.post')) {
                        relevantScope = node;
                        shouldUpdate = true;
                        break;
                    } else {
                        const nestedPosts = node.querySelectorAll && node.querySelectorAll('div.post');
                        if (nestedPosts && nestedPosts.length > 0) {
                            relevantScope = node;
                            shouldUpdate = true;
                            break;
                        }
                    }
                }
            }
            if (shouldUpdate) break;
        }
    }
 
    if (shouldUpdate) {
        requestAnimationFrame(() => {
            applyPlusHandlers(relevantScope);
            hideAllNewPostVotes(relevantScope);
            scheduleUpdate(relevantScope);
        });
    }
});
 
observer.observe(document.body, { childList: true, subtree: true });
 
// Резервный таймер для обновления раз в секунду
const backupInterval = setInterval(combinedBackupUpdate, 1000);
</script>
Список изменений:

Добавлена проверка if (!$('#pun-viewtopic').length) return; в следующие места:

    ✅ getPostIds() - не получает ID постов вне топика
    ✅ setPlus() - не обрабатывает клики вне топика 
    ✅ BR() - обёртка для кликов
    ✅ hideAllNewPostVotes() - не скрывает элементы вне топика
    ✅ applyPlusHandlers() - не назначает обработчики вне топика
    ✅ insertThanksList() - не вставляет списки вне топика
    ✅ processVotes() - не обрабатывает ответы вне топика
    ✅ fetchVotesForPosts() - не делает запросы вне топика
    ✅ scheduleUpdate() - не планирует обновления вне топика
    ✅ combinedBackupUpdate() - не обновляет вне топика
    ✅ $(function() {...}) - не инициализирует вне топика
    ✅ document.addEventListener('pun_post', ...) - не реагирует на события вне топика
    ✅ document.addEventListener('vote:happened', ...) - не обрабатывает лайки вне топика
    ✅ $(document).on('ajaxSuccess', ...) - не слушает AJAX вне топика
    ✅ MutationObserver callback - не наблюдает за изменениями вне топика

+1

1746

@Alex_63
Смотри
есть твой мгновенный предпросмотр сообщений десятилетней давности
есть сервисные пользовательские теги
Как можно догадаться, из-за локального парса и конкретно того, что локальный парс ничего не знает про кастомные теги, указанные для конкретного форума, этот предпросмотр парсить "пользовательские теги" не может
Человеки отчаянно хотят именно мгновенный предпросмотр, который свитчится после каждого введенного символа, т.е. если парсить это на стороне севриса - это прям больше 1 рпс с тела может быть, и это прям не очень хорошо выглядит, я на 99.9% уверена, что это будет отбиваться рпс-лимитером.
В связи с этим, вопрос:
есть ли опция каким-либо образом вытащить функцию, аналогично сервисному механизму выпаршивающую пользовательские теги, или выход один - страдать юзать старую скриптовую реализацию того, что можно реализовать сервисными тегами, просто ради того, чтобы оно парсилось на клиенте?
Я не хочу даже пытаться написать с нуля то, что реализовано на стороне сервиса, да)))
Отдельный мем в том, что как будто есть желание условно шаблонные приколы запихать в пользовательские теги, чтобы не лепить html в каждом посте, но для каждого такого тега дорабатывать парс в мгновенном предпросмотре я явным образом не хочу.

Отредактировано Амираль (Вс, 1 Фев 2026 00:47:41)

0

1747

Амираль
Сразу скажу, что мгновенный предпросмотр точно не планируется к доработке, вместо него предлагается использовать визуальный редактор, в него можно и пользовательские теги добавить, по типу

Код:
WYSI.extend({
    'indent': ['.custom_tag_indent', '[indent]', ''],
    'float': ['.custom_tag_float', '[float]', '[/float]', function(startTag,elem) {
        return BBQuote.addAttribute(startTag, elem.attr('alt'))
    }]
});

С функцией из парсера в движке сложнее, я посмотрю на досуге что можно сделать. Сразу подумалось, что как минимум придётся ещё раз те же теги прописывать, что и в поле Пользовательские BB-теги, только уже передавать их в эту функцию. Как я понимаю, у тебя идея в том чтобы просто перевести с php на js то, что в парсере по части пользовательских тегов.

+1

1748

Alex_63 написал(а):

Амираль
Сразу скажу, что мгновенный предпросмотр точно не планируется к доработке, вместо него предлагается использовать визуальный редактор, в него можно и пользовательские теги добавить, по типу

Код:
WYSI.extend({
    'indent': ['.custom_tag_indent', '[indent]', ''],
    'float': ['.custom_tag_float', '[float]', '[/float]', function(startTag,elem) {
        return BBQuote.addAttribute(startTag, elem.attr('alt'))
    }]
});

С функцией из парсера в движке сложнее, я посмотрю на досуге что можно сделать. Сразу подумалось, что как минимум придётся ещё раз те же теги прописывать, что и в поле Пользовательские BB-теги, только уже передавать их в эту функцию. Как я понимаю, у тебя идея в том чтобы просто перевести с php на js то, что в парсере по части пользовательских тегов.

С идеей ты прав, да
Про визуальный идею закину)

+1

1749

Merlin777 написал(а):

❤︎Чёрный Список❤︎

Представляем вашему вниманию универсальный скрипт "Чёрный список" (далее - ЧС), который помогает форумчанам игнорировать друг друга

 
Выглядит супер и работает. Только почему то эта функция доступна и гостям

0

1750

Merlin777 написал(а):

❤︎Чёрный Список❤︎

А вставлять на форум нужно только тот код что в HTML верх идет или CSS тоже?

0

1751

СергейSerg написал(а):

вставлять на форум нужно только тот код что в HTML верх идет или CSS тоже?

Если вы имеете в виду вот это:

<!-- Чёрный список -->
<link rel="stylesheet" href="https://forumstatic.ru/files/001a/f0/7d/46139.css?v=5">
<script src="https://forumstatic.ru/files/001a/f0/7d/48003.js?v=110"></script>
<!-- Чёрный список -->

То тут первая строчка - это ссылка на CSS, а вторая - cсылка на скрипт:) 

Эти две строчки дадут сразу готовый дизайн, а если хотите свой - то ставите и модифицируете отдельно CSS и JS, которые приведены там ниже.


СергейSerg написал(а):

Выглядит супер и работает. Только почему то эта функция доступна и гостям

Эта функция не должна быть доступна гостям, так как у Гостей нет своего профиля.
Если у вас Гости видят чёрный список, то покажите скриншот, будем разбираться:)

+3

1752

Merlin777
Меня интересует чуть другой вопрос. А есть возможность сделать скрипт для темной темы для дизайна "Classic DarkCyan"? Был бы признателен!

0

1753

СергейSerg написал(а):

Merlin777
Меня интересует чуть другой вопрос. А есть возможность сделать скрипт для темной темы для дизайна "Classic DarkCyan"? Был бы признателен!

Без проблем, вот 2 варианта:)

Тёмная тема для Classic DarkCyan v.1
Код:
 <!-- Тёмная тема для Classic DarkCyan v.1-->
<style>
/* ===== Переключатель темы ===== */
#html-header {
    position: relative;
}
.StyleSwitch {
    height: 20px;
    width: 20px;
    position: absolute;
    z-index: 1000;
    right: 5px;
    top: 50px;
    border-radius: 50%;
    cursor: pointer;
    box-shadow: 0 0 10px rgba(0,0,0,.7);
    transition: .1s ease;
}
.StyleSwitch:hover { opacity: .8; }
.StyleSwitch.St {
    background: #2DA7A7 url(https://forumstatic.ru/files/0016/a6/f8/22564.png) center center no-repeat!important;
    background-size: 16px!important;
}
.StyleSwitch.My {
    background: #1a6b6b url(https://forumstatic.ru/files/0016/a6/f8/90261.png) center center no-repeat!important;
    background-size: 16px!important;
}

/* ===== Тёмная тема Classic DarkCyan ===== */
body.dark-theme {
    background: #1a2a2a;
    color: #c8e0e0;
}
body.dark-theme #pun {
    background: #1e2e2e;
    box-shadow: 0 0 15px rgba(0, 200, 200, .1);
}
body.dark-theme #pun-title,
body.dark-theme #pun-title .container {
    background-color: #1a5a5a;
    background-image: linear-gradient(to bottom, rgba(0,0,0,.3) 0%, rgba(0,0,0,0) 70%);
}
body.dark-theme #pun-title h1 span {
    color: #e0f5f5;
    text-shadow: 0 2px 4px rgba(0,0,0,.5);
}
body.dark-theme #pun-navlinks {
    background: rgba(0,0,0,.6);
    box-shadow: inset rgba(0,0,0,.8) 0 1px 5px, rgba(0,200,200,.05) 0 -1px 0;
}
body.dark-theme #pun-navlinks .container {
    color: #b0d8d8;
}
body.dark-theme #pun-navlinks li a {
    color: #c8e8e8;
    text-shadow: 0 1px 3px rgba(0,0,0,.5);
}
body.dark-theme #pun-navlinks li a:hover {
    background: rgba(45, 167, 167, .2);
    color: #80e0e0;
    text-shadow: 0 0 8px rgba(100, 220, 220, .5);
}
body.dark-theme #pun-navlinks .isactive a {
    background: rgba(45, 167, 167, .35);
    box-shadow: inset rgba(0,200,200,.15) 0 1px 4px;
}
body.dark-theme #pun-ulinks .container {
    background: #1a3a3a;
    background-image: linear-gradient(to bottom, rgba(255,255,255,.05) 0%, rgba(255,255,255,0) 100%);
    border-bottom-color: #2a5a5a;
}
body.dark-theme #pun-ulinks li a {
    color: #70c8c0;
}
body.dark-theme #pun-ulinks li a:hover {
    background: rgba(45, 167, 167, .15);
    box-shadow: inset rgba(0,0,0,.2) 0 1px 3px;
}
body.dark-theme .punbb .main .container,
body.dark-theme .punbb .post .container {
    background: #223333;
    border-color: #2a5050;
    color: #c0d8d8;
}
body.dark-theme .punbb .section .container,
body.dark-theme .punbb .modmenu .container,
body.dark-theme .punbb .post-body,
body.dark-theme .punbb .post-links,
body.dark-theme .punbb td.tc2,
body.dark-theme .punbb td.tc3,
body.dark-theme .punbb .formal fieldset .post-box,
body.dark-theme #viewprofile li strong,
body.dark-theme #viewprofile li div,
body.dark-theme #setmods dd,
body.dark-theme .punbb .info-box,
body.dark-theme .punbb #pun-main .info-box .legend {
    background: #1e3030;
    color: #b8d0d0;
}
body.dark-theme #pun-stats h2,
body.dark-theme .punbb .main h1,
body.dark-theme .punbb .main h2,
body.dark-theme #pun-debug h2,
body.dark-theme .punbb-admin #pun-admain h2 {
    background-color: #1a5a5a;
    border-color: #2a7a7a;
    color: #d0f0f0;
    background-image: linear-gradient(to bottom, rgba(255,255,255,.08) 0%, rgba(255,255,255,0) 100%);
    text-shadow: 0 1px 2px rgba(0,0,0,.6);
    box-shadow: inset rgba(100,220,220,.1) 0 1px 0;
}
body.dark-theme #pun-stats h2 span,
body.dark-theme .punbb .main h1 span,
body.dark-theme .punbb .main h2 span {
    color: #d0f0f0;
}
body.dark-theme .punbb a,
body.dark-theme .punbb a:link,
body.dark-theme .punbb a:visited {
    color: #50b8b0;
}
body.dark-theme .punbb a:hover,
body.dark-theme .punbb a:focus,
body.dark-theme .punbb a:active {
    color: #70e0d8;
    text-decoration: underline;
}
body.dark-theme .punbb li.isactive a,
body.dark-theme .punbb li.isactive a:link,
body.dark-theme .punbb li.isactive a:visited {
    color: #a0d0d0;
}
body.dark-theme .punbb td,
body.dark-theme .punbb fieldset,
body.dark-theme #viewprofile ul,
body.dark-theme #profilenav ul,
body.dark-theme .punbb .post .post-body,
body.dark-theme .punbb .post h3 span,
body.dark-theme .post-links ul,
body.dark-theme .post-links,
body.dark-theme .usertable table,
body.dark-theme .punbb .container,
body.dark-theme .punbb .post-body,
body.dark-theme .post h3,
body.dark-theme #pun-title {
    border-color: #2a5050;
}
body.dark-theme .punbb th {
    background-color: #1a3a3a;
    background-image: linear-gradient(to bottom, rgba(255,255,255,.06) 0%, rgba(255,255,255,0) 100%);
    border-bottom-color: #2a5a5a !important;
    color: #a0c8c8;
}
body.dark-theme .punbb .post h3 {
    background: #1a3030;
    border-color: #2a5050;
}
body.dark-theme .punbb .post h3 span {
    background-color: transparent !important;
    color: #b0d0d0;
    border-left-color: #2a5050;
}
body.dark-theme .punbb .quote-box,
body.dark-theme .punbb .code-box {
    border-color: #2a5a5a;
    background-color: #1a2a2a;
    color: #c0d8d8;
    box-shadow: 0 1px 6px rgba(0,0,0,.3);
}
body.dark-theme .punbb .quote-box cite,
body.dark-theme .punbb .code-box .legend,
body.dark-theme .punbb .spoiler-box > div {
    background: #1e3a3a;
    background-image: linear-gradient(to bottom, rgba(255,255,255,.05) 0%, rgba(255,255,255,0) 100%);
    border-bottom-color: #2a5a5a;
    color: #70b8b0;
}
body.dark-theme .punbb .post-content .blockcode,
body.dark-theme .punbb .post-content blockquote {
    background-color: #182828;
}
body.dark-theme .punbb .code-box {
    background-color: #162626;
    color: #d0e8e8;
}
body.dark-theme form .button,
body.dark-theme input[type="submit"] {
    color: #a0d8d0;
    background: #1a3a3a;
    border-color: #2a5a5a;
    background-image: linear-gradient(to bottom, rgba(255,255,255,.08) 0%, rgba(255,255,255,0) 100%);
    text-shadow: 0 1px 2px rgba(0,0,0,.4);
    box-shadow: 0 1px 3px rgba(0,0,0,.3);
}
body.dark-theme form .button:hover,
body.dark-theme input[type="submit"]:hover {
    background: #1e4a4a;
    box-shadow: inset rgba(100,220,220,.1) 0 100px 0, 0 1px 3px rgba(0,0,0,.3);
}
body.dark-theme form .button:disabled,
body.dark-theme input[type="submit"]:disabled {
    background: #0e1e1e;
    color: #556;
    box-shadow: none;
}
body.dark-theme .punbb input[type=password],
body.dark-theme .punbb input[type=text],
body.dark-theme .punbb input[type=search],
body.dark-theme .punbb select,
body.dark-theme .punbb textarea {
    background: #162626;
    border-color: #2a4a4a !important;
    color: #b8d0d0;
}
body.dark-theme .punbb input[type=password]:hover,
body.dark-theme .punbb input[type=text]:hover,
body.dark-theme .punbb input[type=search]:hover,
body.dark-theme .punbb textarea:hover,
body.dark-theme .punbb select:hover,
body.dark-theme .punbb input[type=password]:focus,
body.dark-theme .punbb input[type=text]:focus,
body.dark-theme .punbb input[type=search]:focus,
body.dark-theme .punbb textarea:focus,
body.dark-theme .punbb select:focus {
    border-color: #3a7a7a !important;
}
body.dark-theme #viewprofile li,
body.dark-theme #setmods dl {
    background: #1a3030;
    background-image: linear-gradient(to bottom, rgba(255,255,255,.04) 0%, rgba(255,255,255,0) 100%);
    color: #b0c8c8;
}
body.dark-theme #viewprofile h2 span,
body.dark-theme #profilenav h2 span,
body.dark-theme .punbb legend span {
    background-color: #1e3030;
    color: #70c8c0;
}
body.dark-theme #profilenav li a:hover {
    background: rgba(45, 167, 167, .15);
    box-shadow: inset rgba(0,0,0,.15) 0 1px 3px;
    color: #70e0d8;
}
body.dark-theme #profilenav li.isactive a {
    background: rgba(45, 167, 167, .12);
    box-shadow: inset rgba(100,200,200,.08) 0 1px 3px;
}
body.dark-theme .punbb .divider {
    border-color: #2a4a4a transparent transparent;
}
body.dark-theme li#onlinelist {
    border-top-color: #2a4a4a;
}
body.dark-theme li#onlinelist div {
    border-top-color: #2a4a4a;
    color: #a0b8b8;
}
body.dark-theme #pun-announcement h2 span {
    color: #c0e0e0;
    border-bottom-color: #2a5a5a;
}
body.dark-theme #pun-announcement .container {
    border-bottom-color: rgba(255,255,255,.05) !important;
}
body.dark-theme #pun-crumbs1 p.container {
    background: #1e2e2e;
    border-top-color: #2a4a4a;
    color: #a0b8b8;
}
body.dark-theme #pun-crumbs2 .container {
    background: #1e2e2e;
    border-bottom-color: #2a4a4a;
    color: #a0b8b8;
}
body.dark-theme #pun-about .container {
    background: #1e2e2e;
    border-top-color: #2a4a4a;
}
body.dark-theme #pun-status .container {
    background: #1e3030;
    border-bottom-color: #2a4a4a;
    color: #a0b8b8;
}
body.dark-theme hr {
    border-color: #2a4a4a;
}
body.dark-theme .punbb .modmenu .container {
    background-color: transparent;
    color: #a0b8b8;
}
body.dark-theme .post-links li a:hover {
    background-color: rgba(45, 167, 167, .12);
    border-color: #2a5a5a;
    color: #70d8d0 !important;
}
body.dark-theme #pun-pagelinks {
    color: #b8d0d0;
}
body.dark-theme #pun-pagelinks a:active,
body.dark-theme #pun-pagelinks a:focus {
    background-color: #2a5a5a;
    color: #e0f5f5;
}
body.dark-theme .punbb p.container.crumbs a:hover {
    background-color: rgba(45, 167, 167, .12);
    border-color: #2a5a5a;
}
body.dark-theme #paginator {
    background-color: #1a2a2a !important;
    border-color: #2a5a5a !important;
    box-shadow: 0 5px 10px rgba(0,0,0,.5) !important;
}
body.dark-theme #paginator a:hover {
    background-color: #1e4a4a !important;
}
body.dark-theme .punbb .post .post-rating .container,
body.dark-theme .punbb .post-vote .container {
    background-color: rgba(0, 40, 40, .4) !important;
    border-color: transparent !important;
}
body.dark-theme .punbb .post-sig dt {
    border-top-color: #2a4a4a;
}
body.dark-theme #profile-signature ul {
    border-color: #2a5a5a;
}
body.dark-theme .user-agent [class^="ua-icon-"]:before {
    filter: grayscale(.2) brightness(1.3);
}
body.dark-theme fieldset {
    background: #1a3030 !important;
    border-color: #2a5a5a;
}
body.dark-theme .punbb .post.new-ajax {
    background-color: rgba(0, 80, 80, .15);
}
body.dark-theme #keyboard-span input {
    background-color: rgba(255,255,255,.03);
    border-color: #2a4a4a;
    color: #c0d8d8;
}
body.dark-theme #profilenav li.isactive {
    background-color: rgba(255,255,255,.05);
    border-color: rgba(0,0,0,.1);
}
body.dark-theme .post-content table td {
    border-color: #2a5a5a !important;
}
body.dark-theme ul.punbb-dropdown ul {
    background-color: #1a3030;
}
body.dark-theme #isk {
    background: rgba(0,0,0,.3);
    border-color: transparent;
}
body.dark-theme .post-content kbd {
    background: #162626 !important;
    border-color: transparent !important;
}
body.dark-theme .punbb #MyBookmarks {
    background: #1e2e2e;
}
body.dark-theme .punbb #MyBookmarks li.BookmarkL {
    border-bottom-color: #2a5a5a !important;
}
body.dark-theme .punbb #MyBookmarks span.scrl {
    background-color: #162626;
    color: #b0c8c8;
}
body.dark-theme .punbb .editBookmark h1,
body.dark-theme .punbb .main #MyBookmarks h2,
body.dark-theme .punbb .main #MyBookmarks h1 {
    background-color: #1a3a3a !important;
    color: #b8d0d0 !important;
}
body.dark-theme .punbb .editBookmark .container {
    background-color: #1e2e2e;
}
body.dark-theme .punbb #navextra1:hover > a,
body.dark-theme .punbb #pun-navlinks ul ul {
    background-color: #1a4a4a !important;
    border-color: #2a5a5a;
    box-shadow: none !important;
}
body.dark-theme #pun-navlinks ul ul li:hover {
    background-color: #1e5a5a !important;
}
body.dark-theme .tooltipsy {
    background-color: #1a3a3a !important;
}
body.dark-theme .pun-modal .modal-inner {
    background: #1e2e2e;
}
body.dark-theme .punbb div[id$="-area"] {
    background-color: #162626 !important;
    border-color: rgba(255,255,255,.05) !important;
}
body.dark-theme .punbb fieldset {
    border-color: rgba(255,255,255,.08);
}
body.dark-theme .category th,
body.dark-theme #pun-viewforum .main th,
body.dark-theme #pun-searchtopics .main th {
    color: #a0c8c8;
}
body.dark-theme #table-area td {
    background-color: #1a3a3a;
    border-color: #2a5a5a !important;
}
body.dark-theme #table-area td.selected {
    background-color: #2a6a6a;
    border-color: #1a3a3a !important;
}
body.dark-theme #poll-special h1 {
    background: #1a5a5a;
}
body.dark-theme #pun-index .main h2,
body.dark-theme #pun-searchtopics .main h1,
body.dark-theme #pun-viewforum .main h1 {
    background: transparent;
    color: #c0e0e0;
}
body.dark-theme #pun-main h3 > a {
    background: #1a3a3a !important;
    border-radius: 10px !important;
    padding: 0 7px 3px !important;
}
body.dark-theme #setmods dl,
body.dark-theme .punbb .post h3 {
    background-color: rgba(0,0,0,.15);
    color: #a0b8b8;
}
body.dark-theme #pun-redirect {
    background: #1e2e2e;
}
body.dark-theme #form-buttons td:hover {
    background-color: #162626;
}
body.dark-theme #pun-userlist th {
    background: #1a3a3a !important;
}
body.dark-theme #pun-announcement h2 > span {
    color: #c0e0e0;
    border-bottom-color: #2a5a5a;
}
body.dark-theme #pun-debug h2,
body.dark-theme #pun-stats h2,
body.dark-theme .punbb .main h1,
body.dark-theme .punbb .main h2,
body.dark-theme .punbb-admin #pun-admain h2 {
    border-color: #2a7a7a;
}
body.dark-theme li.pa-online {
    border-left-color: #30a0a0;
}
body.dark-theme .adcontainer {
    border-color: #2a5a5a !important;
}
body.dark-theme .publication {
    color: #70c8c0 !important;
    text-shadow: none !important;
}
body.dark-theme .slider_wrap figcaption {
    background: rgba(10, 20, 20, .9) !important;
}
body.dark-theme .post-author .pa-online,
body.dark-theme .post-author:not(.online) .pa-author .acchide {
    background: #3a8a70 !important;
}
body.dark-theme #pun-searchform .main input[type=search] {
    background-color: rgba(0,0,0,.3);
}
body.dark-theme .input-button[disabled] {
    opacity: .5 !important;
}
body.dark-theme #pun-edit a:not([disabled]) {
    color: #5a9a90;
}
body.dark-theme form li#navblock-2 a:hover,
body.dark-theme a[href*="/pun/viewtopic"]:not([disabled]) {
    background-color: rgba(26, 58, 58, .4);
}
body.dark-theme #profilenav ul {
    border-bottom-color: #2a5a5a;
}
body.dark-theme #profile-right li:not(:last-child) {
    border-bottom-color: #2a4a4a;
}
body.dark-theme #pun-redirect .info a {
    color: #3a8a8a;
}
body.dark-theme .punbb .closedatafield + a,
body.dark-theme .punbb .closedatafield + strong > a {
    color: #888 !important;
}
body.dark-theme #pun-ulinks {
    background: #162626;
}
body.dark-theme .punbb .stickytext + a,
body.dark-theme .punbb .stickytext + strong > a,
body.dark-theme .punbb .stickytext + .closedatafield + a,
body.dark-theme .punbb .stickytext + .closedatafield + strong > a {
    color: #c8a0a0 !important;
}
body.dark-theme #pun-searchtopics.punbb .tcl a {
    color: #80b8b8 !important;
}
body.dark-theme .quote-box .quote-after.q-resize-1,
body.dark-theme #messages .post .quote-box.answer-box blockquote:not(.blockquote-toggle)::after {
    background: linear-gradient(to bottom, transparent 0%, #1a3a3a 80%, #1a3a3a 100%) !important;
}
body.dark-theme #pun-messages .main #messages tr:hover {
    background-color: rgba(255,255,255,.03);
}
body.dark-theme .usersname a,
body.dark-theme #onlinelist a,
body.dark-theme li.pa-author > a {
    filter: brightness(1.15) contrast(1.1);
}
body.dark-theme #pun-rules .main .html-box > p,
body.dark-theme #pun-regrules .main .html-box > p,
body.dark-theme #pun-register .main .info-box {
    background: #2a2a3a;
    border-color: #4a4a5a;
}
body.dark-theme #pun-rules font[color=grey],
body.dark-theme #pun-regrules font[color=grey] {
    color: #e0645a;
}
body.dark-theme #pun-rules span[style="color:red"],
body.dark-theme #pun-regrules span[style="color:red"] {
    color: #8bc34a !important;
}
body.dark-theme #pun-rules .main .html-box div > p,
body.dark-theme #pun-regrules .main .html-box div > p,
body.dark-theme #pun-rules .main .html-box span > a,
body.dark-theme #pun-regrules .main .html-box span > a {
    background: rgba(22, 38, 38) !important;
    border-color: rgba(0,0,0,.4);
}
body.dark-theme #pun-regrules .main p.formsubmit {
    background: transparent;
    border-color: transparent;
}
body.dark-theme .slider_wrap {
    box-shadow: 0 7px 10px rgba(0,0,0,.5) !important;
}
body.dark-theme #pun-pagelinks a:focus,
body.dark-theme #pun-pagelinks a:active {
    background-color: rgba(45, 167, 167, .5);
}
body.dark-theme [id^="tooltipsy"] {
    max-width: 90vw;
}
body.dark-theme .post-content .code-box .linenums li.L1,
body.dark-theme .post-content .code-box .linenums li.L3,
body.dark-theme .post-content .code-box .linenums li.L5,
body.dark-theme .post-content .code-box .linenums li.L7,
body.dark-theme .post-content .code-box .linenums li.L9 {
    background: transparent !important;
}
body.dark-theme .code-box .linenums .str { color: #6ab88a; }
body.dark-theme .code-box .linenums .kwd { color: #7a90b0; }
body.dark-theme .code-box .linenums .com { color: #c08a8a; }
body.dark-theme .code-box .linenums .typ { color: #c08090; }
body.dark-theme .code-box .linenums .lit { color: #60b0a8; }
body.dark-theme .code-box .linenums .clo,
body.dark-theme .code-box .linenums .opn,
body.dark-theme .code-box .linenums .pun { color: #c0a060; }
body.dark-theme .code-box .linenums .tag { color: #60b8d0; }
body.dark-theme .code-box .linenums .atn { color: #a880b8; }
body.dark-theme .code-box .linenums .atv { color: #90b870; }
body.dark-theme .code-box .linenums .pln { color: #c8d8d8; }

/* ===== Подсветка ссылок в постах ===== */
body.dark-theme .punbb .post-content p a {
    background: rgba(45, 167, 167, .08);
    border-radius: 3px;
    padding: 2px;
}
body.dark-theme .punbb .post-content p a:hover {
    color: #70e0d8;
}

/* ===== "Сказали спасибо" ===== */
body.dark-theme .postVoters strong {
    background: #1a4a4a;
    padding: 2px 6px;
    border-radius: 4px;
    color: #80d0c8;
    border: 1px solid #2a6a6a;
}

/* ===== Треугольники ===== */
body.dark-theme .post .post-body::before {
    border-color: transparent #1e3030 transparent transparent !important;
}
body.dark-theme .post .post-body::after {
    border-color: transparent #2a5050 transparent transparent !important;
}

/* ===== Checkbox/Radio ===== */
body.dark-theme input[type=checkbox],
body.dark-theme input[type=radio] {
    border-color: rgba(255,255,255,.1);
    background-color: rgba(0,0,0,.15);
}
body.dark-theme input[type=radio]:hover,
body.dark-theme input[type=checkbox]:hover {
    border-color: rgba(255,255,255,.25);
}
body.dark-theme input[type=radio]:checked:after {
    background: #c0d8d8;
}
body.dark-theme input[type=checkbox]:focus,
body.dark-theme input[type=radio]:focus {
    border-color: #c0d8d8;
}
body.dark-theme input[type=checkbox]:checked::after {
    color: #e0f0f0;
}

/* ===== Scrollbar ===== */
body.dark-theme *::-webkit-scrollbar {
    -webkit-appearance: none;
}
body.dark-theme *::-webkit-scrollbar:vertical {
    width: 8px;
}
body.dark-theme *::-webkit-scrollbar:horizontal {
    height: 8px;
}
body.dark-theme *::-webkit-scrollbar-thumb {
    background-color: rgba(80, 180, 170, .4);
    border-radius: 10px;
}
body.dark-theme *::-webkit-scrollbar-thumb:hover {
    background-color: rgba(100, 210, 200, .5);
}
body.dark-theme *::-webkit-scrollbar-thumb:active {
    background-color: rgba(120, 230, 220, .5);
}
body.dark-theme *::-webkit-scrollbar-track {
    background-color: rgba(255,255,255,.03);
    border-radius: 10px;
}
body.dark-theme *::-webkit-scrollbar-corner {
    background-color: transparent;
}

/* ===== Placeholder ===== */
body.dark-theme ::placeholder {
    color: rgba(200, 224, 224, .6);
}

/* ===== Post colors ===== */
body.dark-theme .post span[style="color: red"] { color: #ff8989 !important; }
body.dark-theme .post span[style="color: green"] { color: #6ab88a !important; }
body.dark-theme .post span[style="color: blue"] { color: #50a8e0 !important; }
body.dark-theme .post span[style="color: maroon"] { color: #c08090 !important; }
body.dark-theme .post span[style="color: olive"] { color: #d8d870 !important; }
body.dark-theme .post span[style="color: navy"] { color: #9090d8 !important; }

/* ===== Rating hearts ===== */
body.dark-theme div .post-rating p a {
    background: url(https://i.ibb.co/1Gx3mJ9Q/gray-heart.png) no-repeat center !important;
    background-size: 36px auto !important;
    filter: brightness(1.2) contrast(1.1);
}

/* ===== Animations ===== */
@keyframes fadebg-dark {
    0% { background-color: rgba(0, 80, 80, .15); }
    100% { background-color: #1e2e2e; }
}
body.dark-theme .punbb .post.new-ajax {
    animation: fadebg-dark 3s 3s ease-out forwards;
}

/* ===== Mobile ===== */
@media screen and (max-width: 540px) {
    body.dark-theme #pun .punbb #pun-main td {
        border-bottom-color: #2a4a4a !important;
    }
    body.dark-theme #pun-ulinks,
    body.dark-theme #pun-navlinks .container li a {
        filter: brightness(0.85);
    }
    body.dark-theme .punbb #pun-navlinks {
        top: -38px !important;
        background-color: #162626;
        border-bottom: 1px solid #162626 !important;
    }
    body.dark-theme .punbb .category .container,
    body.dark-theme .punbb .forum .container {
        border-top-color: #2a5a5a !important;
    }
    body.dark-theme #pun .punbb .post-author,
    body.dark-theme #pun .punbb .post-author ul {
        background-color: rgba(0,0,0,.1) !important;
    }
    body.dark-theme #pun .punbb .post h3 span {
        background-color: transparent !important;
        border-top: none !important;
    }
    body.dark-theme .punbb .post h3 {
        border-top-color: rgba(0,0,0,.1) !important;
    }
    body.dark-theme #paginator {
        border-color: rgba(255,255,255,.15) !important;
    }
    body.dark-theme #paginator td {
        border-color: rgba(255,255,255,.15) !important;
    }
    body.dark-theme #profile-right li {
        background-color: rgba(0,0,0,.15) !important;
        border-left: none !important;
    }
    body.dark-theme #pun .punbb tr.isticky div.icon,
    body.dark-theme #pun .punbb tr.inewisticky div.icon {
        filter: brightness(1.5) contrast(1.2);
        opacity: .7;
    }
}
</style>

<script type="text/javascript">
(function () {
    /* ===== Функция включения тёмной темы ===== */
    function setStyle () {
        $('.StyleSwitch').removeClass('St').addClass('My');
        $('.StyleSwitch').attr('title','Стандартный стиль Classic DarkCyan');
        $('body').addClass('dark-theme');
    }

    /* ===== Добавляем кнопку в HTML ===== */
    $('#html-header').prepend('<span class="StyleSwitch St" title="Тёмная тема Classic DarkCyan"></span>');
    $('.StyleSwitch').tipsy({fade: true, gravity: 'nw'});

    if($getCookie('StyleSwitch')) { setStyle(); }

    /* ===== Событие клика для включения ===== */
    $('.StyleSwitch.St').live('click', function () {
        $setCookie('StyleSwitch','My',30*3600*24*30*1000);
        setStyle();
    });

    /* ===== Событие клика для выключения ===== */
    $('.StyleSwitch.My').live('click', function () {
        $deleteCookie('StyleSwitch');
        $('.StyleSwitch').addClass('St').removeClass('My');
        $('.StyleSwitch').attr('title','Тёмная тема Classic DarkCyan');
        $('body').removeClass('dark-theme');
    });
})();
</script>
<!-- Конец тёмной темы Classic DarkCyan v.1--->
Тёмная тема для Classic DarkCyan v.2
Код:
<!-- Тёмная тема для Classic DarkCyan v.2-->
<style>
/* ===== Переключатель темы ===== */
#html-header {
    position: relative;
}
.StyleSwitch {
    height: 20px;
    width: 20px;
    position: absolute;
    z-index: 1000;
    right: 5px;
    top: 50px;
    border-radius: 50%;
    cursor: pointer;
    box-shadow: 0 0 10px rgba(0,0,0,.7);
    transition: .1s ease;
}
.StyleSwitch:hover { opacity: .8; }
.StyleSwitch.St {
    background: #426B9A url(https://forumstatic.ru/files/0016/a6/f8/22564.png) center center no-repeat!important;
    background-size: 16px!important;
}
.StyleSwitch.My {
    background: #6996CA url(https://forumstatic.ru/files/0016/a6/f8/90261.png) center center no-repeat!important;
    background-size: 16px!important;
}
 
/* ===== Треугольники (только для тёмной темы) ===== */
body.dark-theme .post .post-body::before {
    border-color: transparent #4a5b70 transparent transparent !important;
}
body.dark-theme .post .post-body::after {
    border-color: transparent #2d3748 transparent transparent !important;
}
 
/* ===== "Сказали спасибо" (только для тёмной темы) ===== */
body.dark-theme .postVoters strong {
    background: #5a7a8f;
    padding: 2px 4px;
    border-radius: 4px;
    color: #e8f0f5;
}
 
/* ===== Подсветка ссылок в постах (только для тёмной темы) ===== */
body.dark-theme .punbb .post-content p a {
    background: rgba(100,150,200,0.15);
    border-radius: 3px;
    padding: 2px;
}
body.dark-theme .punbb .post-content p a:hover { 
    color: #80c8e0;
}
</style>
 
<script type="text/javascript">
(function () {
    var style = 'https://forumstatic.ru/files/001a/f0/7d/73291.css?v=45'; // Ссылка на внешний CSS
    style = '<link rel="stylesheet" id="StyleAdd" type="text/css" href="'+style+'"/>';
 
    /* ===== Функция включения тёмного стиля ===== */
    function setStyle () {
        $('link[rel="stylesheet"]:last').after(style);
        $('.StyleSwitch').removeClass('St').addClass('My');
        $('.StyleSwitch').attr('title','Стандартный стиль');
        $('body').addClass('dark-theme');
    }
 
    /* ===== Добавляем кнопку в HTML ===== */
    $('#html-header').prepend('<span class="StyleSwitch St" title="Затемненный стиль"></span>');
    $('.StyleSwitch').tipsy({fade: true, gravity: 'nw'});
 
    if($getCookie('StyleSwitch')) { setStyle (); }
 
    /* ===== Событие клика по кнопке для включения ===== */
    $('.StyleSwitch.St').live('click', function () {
        $setCookie('StyleSwitch','My',30*3600*24*30*1000);
        setStyle ();
    });  
 
    /* ===== Событие клика по кнопке для выключения ===== */
    $('.StyleSwitch.My').live('click', function () {
        $('link#StyleAdd').remove();
        $deleteCookie('StyleSwitch');
        $('.StyleSwitch').addClass('St').removeClass('My');
        $('.StyleSwitch').attr('title','Затемненный стиль');
        $('body').removeClass('dark-theme');
    });
})();
</script>
<!-- Конец тёмной темы Classic DarkCyan v.2--->

Вариант №1 мне больше понравился :)

+1

1754

Здравствуйте ! Можно ли добавить в форму ответа кнопку " Сохранить черновик" как здесь ?

https://e.radikal.host/2026/05/15/SNIMOK-EKRANA-2026-05-15-232640.md.png

0

1755

Podozrevatel
В теории, можно это сделать с помощью API: при нажатии на кнопку "Сохранить черновик" будет записываться содержимое из формы на сервер для каждого отдельного пользователя каждой отдельной темы. Но такого скрипта нет.
Вы можете воспользоваться более легкой версией этого скрипта, опубликованного здесь: Общие вопросы от новичков (63) #3 (684 сообщение).
Этот скрипт автоматически (при наборе) сохраняет содержимое из формы, но данные сохранятся только в одном браузере и до полной его очистки (от кэша и куков). В остальных браузерах и устройствах сохранённый в форме текст будет отсутствовать.

0

1756

Скрипт "Черновики"
Добавляет кнопку Черновик в форму ответа. При нажатии в конец сообщения добавляется маркер-эмодзи, который определяет уровень доступа к черновику.

Автор видит свой черновик с оранжевой пунктирной рамкой. Остальные пользователи черновик не видят (пост скрыт).

Для публикации: отредактировать сообщение и удалить маркер вручную.

Настройка кнопки: Профиль → аккордеон 📋 Черновики (доступен всем пользователям).

По умолчанию кнопка 📋 Черновики отключена.

Маркеры настроек:

  • 📝 — черновик виден только автору

  • 🔒 — черновик виден автору и администраторам

  • 🛡️ — черновик виден автору, администраторам и модераторам раздела

Варианты установки скрипта:

Черновик 📝 — виден только автору

Установка: В HTML Верх

<!-- Черновик -->
<link rel="stylesheet" href="https://forumstatic.ru/files/001a/f0/7d/17129.css?v=19">
<script src="https://forumstatic.ru/files/001a/f0/7d/46854.js?v=4"></script>
<!-- Черновик -->

Черновик 🔒 — виден автору и администраторам

Установка: В HTML Верх

<!-- Черновик -->
<link rel="stylesheet" href="https://forumstatic.ru/files/001a/f0/7d/17129.css?v=19">
<script src="https://forumstatic.ru/files/001a/f0/7d/42961.js?v=15"></script>
<!-- Черновик -->

Черновик 🛡️ — виден автору, администраторам и модераторам раздела

Установка: В HTML Верх
<!-- Черновик -->
<link rel="stylesheet" href="https://forumstatic.ru/files/001a/f0/7d/17129.css?v=19">
<script src="https://forumstatic.ru/files/001a/f0/7d/89124.js?v=7"></script>
<!-- Черновик -->

Отредактировано Merlin777 (Ср, 20 Май 2026 01:19:35)

0

1757

Скрипт хороший,только при открытии  новой темы кнопки  "черновик" не видно. Нельзя ли её вставить непосредственно в форуму ответа ?

https://upforme.ru/uploads/0000/14/1c/24348/t932582.png

Отредактировано Podozrevatel (Вт, 19 Май 2026 23:05:26)

0

1758

Podozrevatel написал(а):

Скрипт хороший,только при открытии  новой темы кнопки  "черновик" не видно. Нельзя ли её вставить непосредственно в форуму ответа ?

https://upforme.ru/uploads/0000/14/1c/24348/t932582.png

Сейчас можно делать черновиками сообщения при создании и при редактировании. Черновик скрывается только на странице темы (viewtopic.php).

Можно сделать и чтобы скрывалась тема в  списке тем раздела.  (viewforum.php). Но как это будет отображаться? Совсем убирать наверное нехорошо, если есть скрипты, которым важно количество тем в разделе. Скрипты вывода списка тем будут видеть тему и содержание. И скрипты предпросмотра тем тоже будут всё видеть. Ну, будем считать, что их у вас нет:)

Можно при создании скрытого первого сообщения добавлять 📝 🔒 🛡️ в конец (при этом если символ не влазит то заменять последние символы названия). Потом парсить все названия тем на страницах раздела (не путайте со страницами в темах) и скрывать их для "чужих" юзеров.

Или названия тем не скрывать, а вместо них и скрытого содержимого выдавать "заглушку"? 

Ах, да, ещё и на главной странице нужно парсить все темы и заменять черновики тем на заглушки.

Понимаете ли, сделать можно вообще всё. Но в окружающей среде, не предназначенной для таких манипуляций, это всё кроличья нора и очень хрупкие решения, которые в будущем добавляют сложностей:)

P.S. Я добавил возможность задавать "Черновик" и для тем.

P.P.S.  Конечно, это всё делается для удобства пользователей, а не для секьюрити функций. Тексты черновиков в любом случае не засекречены - так как мы не использовали сторонние сервисы и шифрование, просто инфу для удобства положили  чуть поглубже.

Отредактировано Merlin777 (Ср, 20 Май 2026 01:24:54)

+1

1759

Приветствую.
Может кто-то возьмется поправить скрипт Просмотр превью фотографий без перехода на новую страницу на основе плагина iLoad версия 2.6.4 .
@Romych к сожалению давненько не занимается скриптами, весь в творчестве  :cool:
@Deff к сожалению тоже не смог ничем помочь Слайд-шоу в сообщениях на форуме
Нужно что бы работало с:
https://imageban.ru/
https://new.fastpic.org/
https://fastpic.org/

0

1760

Обзор

Реакции на форуме — это инструмент быстрой визуальной обратной связи, позволяющий пользователям выражать эмоции и оценивать полезность сообщений с помощью набора эмодзи или кастомных иконок. Главная ценность этой механики заключается в значительном повышении вовлеченности аудитории: участники могут мгновенно поддержать автора или отметить качественный пост одним кликом, не засоряя тему короткими комментариями вроде «+1» или «спасибо». Внедрение реакций не только оживляет взаимодействие и снижает порог входа для тех, кому лень писать полноценный ответ, но и помогает поддерживать идеальную чистоту и читабельность тредов.

Готовый функционал реакций к постам (как в Xenforo/Invision) для форумов на BestBB/PunBB/mybb.ru. Эмодзи, счётчики, тёмная тема — всё работает.

Исходный код на GitHub: https://github.com/smartor777-sketch/my … -reactions


Что получится

▶ Реакции (3)
    ↓ клик
┌────────────────────────┐
│  👍 1  👎 0  ❤️ 2  😂 0  😮 0  ... │
└────────────────────────┘
10 реакций: 👍 👎 ❤️ 😂 😮 😢 😡 🔥 🎯 💡


Как это выглядит (в развёрнутом виде)

Светлая тема:

https://upforme.ru/uploads/001a/f0/7d/2/700014.webp

Тёмная тема:
https://upforme.ru/uploads/001a/f0/7d/2/324535.webp

❤️ Как это работает на реальном примере - можете посмотреть форум https://kuban-forum.ru (он же https://kuban.mybb.ru).


Схема работы

Развернуть

Форум (MyBB) → Yandex Cloud Function (прокси) → Cloudflare Worker → Durable Object (SQLite)

GET /?post_ids=1,2,3&user_id=456

  • Прокси проверяет Origin, передаёт запрос на Cloudflare Worker

  • Worker проверяет Origin, разбивает post_ids на батчи по 5

  • Для каждого post_id вызывает stub.fetch('/get') у своего DO

  • DO читает reactions из storage, собирает счётчики и возвращает

  • Worker агрегирует ответы → прокси возвращает форуму

GET /?post_id=1&emoji=👍 (список проголосовавших)

  • Прокси передаёт запрос на Worker

  • Worker вызывает stub.fetch('/voters?emoji=👍') у DO

  • DO фильтрует reactions по emoji, возвращает массив user_id

  • Виджет получает user_id → запрашивает имена через API форума → показывает тултип

POST / c телом {post_id, user_id, emoji}

  • Прокси проверяет Origin, передаёт запрос на Worker

  • Worker проверяет Origin, вызывает stub.fetch('/set?user_id=...&emoji=...')

  • DO проверяет: если такой же эмодзи уже стоит — удаляет (toggle off), иначе — записывает

  • DO сохраняет в storage и возвращает обновлённые счётчики

  • Worker → прокси → ответ форуму


Инструкция по установке на Ваш форум "реакций" - легко и по пунктам :)

1. Регистрация на Cloudflare

  • Зайдите на dash.cloudflare.com

  • Зарегистрируйтесь (можно через Google/GitHub)

  • Подтвердите почту

  • Бесплатный тариф — ничего платить не нужно

Важно: В некоторых регионах доступ к сервисам Cloudflare может быть ограничен. Если у вас возникают ошибки при подключении или деплое, используйте VPN.

Что даёт бесплатный тариф:

  • Workers requests  - 100 000/день ~90 000 просмотров страниц (виджет шлёт 1 GET на страницу, клики — отдельные POST)

  • Durable Objects requests - 1 000 000/мес ~5 500 просмотров страниц/день (30 постов на странице, батчи по 5)

  • Durable Objects storage - 1 ГБ ~1 000 000 постов

2. Установка Wrangler (CLI)

Wrangler — официальный CLI для деплоя Cloudflare Workers.

Внимание! Перед установкой убедитесь, что Node.js и npm установлены. Если нет — скачайте с https://nodejs.org

npm install -g wrangler

Проверьте установку:

wrangler --version

Авторизуйтесь:

wrangler login

Браузер откроет страницу Cloudflare — разрешите доступ.

Альтернатива: без Wrangler
Можно деплоить через веб-интерфейс Cloudflare Dashboard, но CLI удобнее.


3. Создание проекта

mkdir forum-reactions && cd forum-reactions
npm init -y

Установите зависимости для сборки:

npm install -D wrangler


4. Файлы проекта

wrangler.jsonc — конфигурация Worker

Код
Код:
{
  "name": "mybb-forum-reactions",
  "main": "src/index.js",
  "compatibility_date": "2025-01-01",
  "durable_objects": {
    "bindings": [
      {
        "name": "REACTIONS_DO",
        "class_name": "ReactionsDO"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_sqlite_classes": ["ReactionsDO"]
    }
  ],
  "compatibility_flags": ["nodejs_compat_v2"],
  "vars": {
    "ALLOWED_ORIGINS": "https://forum1.mybb.ru,https://forum2.mybb.ru"
  }
}

ALLOWED_ORIGINS — белый список доменов форумов, которым разрешено делать запросы. Запросы с других origin'ов получат 403 Forbidden. Если оставить пустым — пропускаются все запросы (удобно для отладки, но небезопасно).

src/index.js — Worker (API Gateway)

Код
Код:
import { ReactionsDO } from './reactions-do.js';

const ALLOWED_ORIGINS_DEFAULT = [];

function getAllowedOrigins(env) {
  const raw = env.ALLOWED_ORIGINS || ALLOWED_ORIGINS_DEFAULT;
  if (typeof raw === 'string') return raw.split(',').map(s => s.trim()).filter(Boolean);
  return raw;
}

function getCorsOrigin(request, allowed) {
  if (!allowed.length) return '*';
  const origin = request.headers.get('Origin');
  if (!origin || origin === 'null') return 'null';
  return allowed.includes(origin) ? origin : 'null';
}

function corsHeaders(corsOrigin) {
  return {
    'Access-Control-Allow-Origin': corsOrigin,
    'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type',
    'Vary': 'Origin',
  };
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);

    if (url.pathname !== '/' && url.pathname !== '/api/reactions') {
      return new Response('Not found', { status: 404 });
    }

    const allowed = getAllowedOrigins(env);
    const corsOrigin = getCorsOrigin(request, allowed);

    if (request.method === 'OPTIONS') {
      return new Response(null, { headers: corsHeaders(corsOrigin) });
    }

    if (corsOrigin === 'null' && allowed.length) {
      return new Response(JSON.stringify({ error: 'Forbidden origin' }), {
        status: 403,
        headers: { 'Content-Type': 'application/json', ...corsHeaders(corsOrigin) },
      });
    }

    if (request.method === 'GET') {
      const postId = url.searchParams.get('post_id');
      const emoji = url.searchParams.get('emoji');

      if (postId && emoji) {
        const id = env.REACTIONS_DO.idFromName(`post:${postId}`);
        const stub = env.REACTIONS_DO.get(id);
        const resp = await stub.fetch(`http://do/voters?emoji=${encodeURIComponent(emoji)}`);
        return new Response(await resp.text(), {
          headers: { 'Content-Type': 'application/json', ...corsHeaders(corsOrigin) },
        });
      }

      const postIds = (url.searchParams.get('post_ids') || '').split(',').filter(Boolean);
      const userId = url.searchParams.get('user_id') || '0';

      const entries = [];
      for (let i = 0; i < postIds.length; i += 5) {
        const batch = postIds.slice(i, i + 5);
        const batchResults = await Promise.all(
          batch.map(async (pid) => {
            const id = env.REACTIONS_DO.idFromName(`post:${pid}`);
            const stub = env.REACTIONS_DO.get(id);
            const resp = await stub.fetch(`http://do/get?user_id=${userId}`);
            return [pid, await resp.json()];
          }),
        );
        entries.push(...batchResults);
      }

      return new Response(JSON.stringify(Object.fromEntries(entries)), {
        headers: { 'Content-Type': 'application/json', ...corsHeaders(corsOrigin) },
      });
    }

    if (request.method === 'POST') {
      try {
        const body = await request.json();
        const { post_id, user_id, emoji } = body;

        const id = env.REACTIONS_DO.idFromName(`post:${post_id}`);
        const stub = env.REACTIONS_DO.get(id);

        const setResp = await stub.fetch(
          `http://do/set?user_id=${encodeURIComponent(user_id)}&emoji=${encodeURIComponent(emoji)}`,
        );

        return new Response(await setResp.text(), {
          status: 200,
          headers: {
            'Content-Type': 'application/json',
            ...corsHeaders(corsOrigin),
            'Cache-Control': 'no-cache, no-store, must-revalidate',
          },
        });
      } catch (postError) {
        return new Response(JSON.stringify({ counts: {}, user_reaction: null }), {
          status: 200,
          headers: { 'Content-Type': 'application/json', ...corsHeaders(corsOrigin) },
        });
      }
    }

    return new Response('Method not allowed', { status: 405 });
  },
};

export { ReactionsDO };

src/reactions-do.js — Durable Object (хранилище реакций)

Код
Код:
function decodeHtmlEntities(str) {
  return str.replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)));
}

export class ReactionsDO {
  constructor(state, env) {
    this.state = state;
    this.env = env;
  }

  async fetch(request) {
    const url = new URL(request.url);

    try {
      if (url.pathname === '/get') {
        const userId = url.searchParams.get('user_id') || '0';
        const raw = (await this.state.storage.get('reactions')) || {};
        const reactions = {};
        for (const [uid, emoji] of Object.entries(raw)) {
          reactions[uid] = decodeHtmlEntities(emoji);
        }
        return this._response(reactions, userId);
      }

      if (url.pathname === '/voters') {
        const emoji = decodeHtmlEntities(url.searchParams.get('emoji'));
        if (!emoji) {
          return new Response('Missing emoji', { status: 400 });
        }
        const raw = (await this.state.storage.get('reactions')) || {};
        const voters = Object.entries(raw)
          .filter(([, e]) => decodeHtmlEntities(e) === emoji)
          .map(([uid]) => uid);
        return new Response(JSON.stringify(voters), {
          headers: { 'Content-Type': 'application/json' },
        });
      }

      if (url.pathname === '/set') {
        const user_id = url.searchParams.get('user_id');
        const emoji = decodeHtmlEntities(url.searchParams.get('emoji'));
        if (!user_id || !emoji) {
          return new Response('Missing params', { status: 400 });
        }

        const raw = (await this.state.storage.get('reactions')) || {};
        const reactions = {};
        for (const [uid, e] of Object.entries(raw)) {
          reactions[uid] = decodeHtmlEntities(e);
        }

        if (reactions[user_id] === emoji) {
          delete reactions[user_id];
        } else {
          reactions[user_id] = emoji;
        }

        await this.state.storage.put('reactions', reactions);
        return this._response(reactions, user_id);
      }

      return new Response('Not found', { status: 404 });
    } catch (error) {
      return new Response(JSON.stringify({ error: error.message }), {
        status: 500,
        headers: { 'Content-Type': 'application/json' },
      });
    }
  }

  _response(reactions, userId) {
    const counts = {};
    let userReaction = null;
    for (const [uid, emoji] of Object.entries(reactions)) {
      counts[emoji] = (counts[emoji] || 0) + 1;
      if (String(uid) === String(userId)) userReaction = emoji;
    }
    return new Response(JSON.stringify({ counts, user_reaction: userReaction }), {
      headers: { 'Content-Type': 'application/json' },
    });
  }
}

5. Деплой

Перед деплоем: откройте wrangler.jsonc и укажите в ALLOWED_ORIGINS домены ваших форумов. Это защитит Worker от спама.

wrangler deploy

После успешного деплоя вы увидите:

Uploaded forum-reactions
Deployed forum-reactions triggers
--

Проверьте, что Worker работает (может потребоваться VPN):

Код:
curl -H "Origin: https://ваш-форум.ru" https://mybb-forum-reactions.ВАШ_АККАУНТ.workers.dev/?post_ids=1

Результат: {"1":{"counts":{},"user_reaction":null}}


6. Yandex Cloud прокси
В некоторых регионах (например, РФ)  Cloudflare может быть недоступен. В этом случае запросы от форума пропускаются через прокси на Yandex Cloud Functions.

Что даёт бесплатный тариф Yandex Cloud Functions:

  • Вызовы функции - 1 000 000/мес ~30 000 просмотров страниц/день (1 GET на загрузку)

  • Исходящий трафик - 10 ГБ/мес ~3 000 000 GET-ответов (~50 байт каждый)

  • После бесплатного лимита — ₽0.20 за 100 000 вызовов.

Создание функции (веб-консоль)

  • Зайдите в console.cloud.yandex.ru

  • Выберите каталог (folder)

  • Cloud Functions → Создать функцию

  • Название: forum-reactions-proxy

  • Нажмите Создать

  • Выберите Редактор кода

  • Среда выполнения: Node.js 18

  • Вставьте код (замените URL и домены на свои):

Код
Код:
const CF_WORKER = 'https://ВАШ_ВОРКЕР.ВАШ_АККАУНТ.workers.dev';

// Разрешённые домены форумов (остальные получат 403)
const ALLOWED_ORIGINS = [
  'https://forum1.mybb.ru',
  'https://forum2.mybb.ru',
];

function getCorsOrigin(origin) {
  if (!origin) return 'null';
  if (ALLOWED_ORIGINS.indexOf(origin) !== -1) return origin;
  return 'null';
}

async function handler(event) {
  const method = event.httpMethod;
  const params = event.queryStringParameters || {};
  const headers = event.headers || {};
  const origin = headers['origin'] || headers['Origin'] || '';
  const corsOrigin = getCorsOrigin(origin);

  const cors = {
    'Access-Control-Allow-Origin': corsOrigin,
    'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type',
    'Vary': 'Origin',
  };

  if (method === 'OPTIONS') {
    return { statusCode: 200, headers: cors, body: '' };
  }

  if (origin && corsOrigin === 'null') {
    return { statusCode: 403, headers: { ...cors, 'Content-Type': 'application/json' },
      body: JSON.stringify({ error: 'Forbidden', origin }) };
  }

  const qs = new URLSearchParams(params).toString();
  const cfUrl = CF_WORKER + '/api/reactions' + (qs ? '?' + qs : '');
  const cfHeaders = {};
  if (headers['origin']) cfHeaders['Origin'] = headers['origin'];
  else if (headers['Origin']) cfHeaders['Origin'] = headers['Origin'];
  if (headers['content-type']) cfHeaders['Content-Type'] = headers['content-type'];
  const cfOptions = { method, headers: cfHeaders };
  if (method === 'POST' && event.body) cfOptions.body = event.body;
  try {
    const cfResp = await fetch(cfUrl, cfOptions);
    const body = await cfResp.text();
    return { statusCode: cfResp.status, headers: { ...cors,
      'Content-Type': cfResp.headers.get('content-type') || 'application/json',
      'Cache-Control': 'no-cache, no-store, must-revalidate',
    }, body };
  } catch (err) {
    return { statusCode: 502, headers: { ...cors, 'Content-Type': 'application/json' },
      body: JSON.stringify({ error: 'Proxy error: ' + err.message }) };
  }
}
module.exports.handler = handler;
  • Нажмите Создать версию

  • После создания откройте функцию → Редактор → вкладка Доступ → разрешите вызов без авторизации

Готово. Функция будет доступна по адресу:
https://functions.yandexcloud.net/ID_ВАШЕЙ_ФУНКЦИИ

Установка через YC CLI (альтернатива)
Установите YC CLI (PowerShell):

Развернуть
Код:
# Скачать и запустить установщик
Invoke-WebRequest -Uri "https://storage.yandexcloud.net/yandexcloud-yc/install.ps1" -OutFile "install.ps1"
powershell -ExecutionPolicy Bypass -File "install.ps1"
# Перезапустите терминал, затем:
yc init

После авторизации создайте функцию:

Развернуть
Код:
yc serverless function create --name forum-reactions-proxy
yc serverless function allow-unauthenticated-invoke forum-reactions-proxy
yc serverless function version create \
  --function-name forum-reactions-proxy \
  --runtime nodejs18 \
  --entrypoint index.handler \
  --memory 128m \
  --execution-timeout 10s \
  --source-path ./index.js

После установки
Подставьте URL функции в скрипт виджета вместо API:

var API = 'https://functions.yandexcloud.net/ID_ВАШЕЙ_ФУНКЦИИ';

Проверьте, что прокси работает:

Код
Код:
curl -H "Origin: https://ваш-форум.ru" https://functions.yandexcloud.net/ID_ВАШЕЙ_ФУНКЦИИ?post_ids=1

Результат: {"1":{"counts":{},"user_reaction":null}}


7. Виджет для форума

Скопируйте этот скрипт и вставьте в HTML-верх форума (Администрирование → Формы → HTML верх).

Перед вставкой: замените API на URL вашего Yandex Cloud прокси.

⚠ Важно: Worker защищает от спама через ALLOWED_ORIGINS (см. wrangler.jsonc). Если ваш форум не в белом списке — запросы будут отклонены.

При наведении на кнопку реакции показывается тултип с именами пользователей ("Прореагировали Даша, Мария и Ольга.").

Читаемый код с пояснениями
Код:
<script>
(function () {
  'use strict';

  var API = 'https://functions.yandexcloud.net/ID_ВАШЕЙ_ФУНКЦИИ';
  var EMOJIS = [
    '\u{1F44D}', // 👍
    '\u{1F44E}', // 👎
    '\u2764\uFE0F',  // ❤️
    '\u{1F602}', // 😂
    '\u{1F62E}', // 😮
    '\u{1F622}', // 😢
    '\u{1F621}', // 😡
    '\u{1F525}', // 🔥
    '\u{1F3AF}', // 🎯
    '\u{1F4A1}', // 💡
  ];

  var CACHE = {};
  var SEQ = {};
  var TRACK = {};

  function decodeHtml(str) {
    if (!str) return str || '';
    return str.replace(/&#(\d+);/g, function (_, code) {
      return String.fromCodePoint(parseInt(code));
    });
  }

  function normalizeCounts(counts) {
    var out = {};
    for (var k in counts) {
      var key = decodeHtml(k);
      out[key] = (out[key] || 0) + counts[k];
    }
    return out;
  }

  EMOJIS = EMOJIS.map(function (e) { return decodeHtml(e); });
  var NAME_CACHE = {};

  function injectCSS() {
    if (document.getElementById('rx-css')) return;
    var s = document.createElement('style');
    s.id = 'rx-css';
    s.textContent =
      '.post-reactions-wrap{margin-top:8px}' +
      '.post-reactions-hdr{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border:1px solid #c5d0e6;border-radius:6px;cursor:pointer;font-size:12px;color:#426B9A;background:#f5f5ff;transition:background .15s;user-select:none}' +
      '.post-reactions-hdr:hover{background:#e0d8f2}' +
      '.pr-arrow{font-size:10px;transition:transform .2s;display:inline-block}' +
      '.pr-arrow.open{transform:rotate(90deg)}' +
      '.pr-count{color:#888}' +
      '.post-reactions{display:flex;flex-wrap:wrap;gap:3px;padding:5px 7px;margin-top:5px;border:1px solid #c5d0e6;border-radius:8px;background:#f8f8ff;align-items:center}' +
      '.reaction-btn{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border:1px solid #d1d1e1;border-radius:5px;cursor:pointer;font-size:13px;line-height:22px;background:#f5f5ff;transition:background .15s;user-select:none;position:relative}' +
      '.reaction-btn:hover{background:#e0d8f2;border-color:#a0b0d0}' +
      '.reaction-btn.active{background:#3e779d;border-color:#3e779d;color:#fff}' +
      '.reaction-btn.loading{opacity:.5;cursor:wait}' +
      '.reaction-btn .emoji{font-size:15px}' +
      '.reaction-btn .count{font-size:11px;font-weight:bold;min-width:10px;text-align:center}' +
      'body.dark-theme .post-reactions-hdr{background:#2d3748;border-color:#4a5b70;color:#7a9cc6}' +
      'body.dark-theme .post-reactions-hdr:hover{background:#3a4a5e}' +
      'body.dark-theme .post-reactions{border-color:#4a5b70;background:#262f40}' +
      'body.dark-theme .reaction-btn{background:#2d3748;border-color:#4a5b70;color:#e2e8f0}' +
      'body.dark-theme .reaction-btn:hover{background:#3a4a5e;border-color:#5a6b80}' +
      'body.dark-theme .reaction-btn.active{background:#426B9A;border-color:#426B9A;color:#fff}' +
      'body.dark-theme .pr-count{color:#8899aa}' +
      '.rx-tooltip{position:absolute;bottom:100%;left:50%;transform:translateX(-50%);background:#333;color:#fff;padding:4px 8px;border-radius:4px;font-size:11px;white-space:nowrap;z-index:999;pointer-events:none;margin-bottom:4px}' +
      'body.dark-theme .rx-tooltip{background:#1a1a2e}';
    document.head.appendChild(s);
  }

  function uid() {
    var x;
    if (typeof FORUM !== 'undefined' && FORUM.get) x = FORUM.get('UserID');
    if (!x && typeof UserID !== 'undefined') x = UserID;
    return x || 0;
  }

  function isGuest() {
    var g;
    if (typeof FORUM !== 'undefined' && FORUM.get) g = FORUM.get('GroupID');
    if (!g && typeof GroupID !== 'undefined') g = GroupID;
    return String(g || 3) === '3';
  }

  function postIds() {
    return Array.from(document.querySelectorAll('.post[id^="p"]'))
      .map(function (e) { return e.id.slice(1); });
  }

  function totalCount(counts) {
    var t = 0;
    EMOJIS.forEach(function (e) { t += counts[decodeHtml(e)] || 0; });
    return t;
  }

  function fetchAll(ids, uid) {
    if (!ids.length) return Promise.resolve({});
    return fetch(API + '?post_ids=' + ids.join(',') + '&user_id=' + uid)
      .then(function (r) { return r.json(); })
      .catch(function () { return {}; });
  }

  function toggle(pid, uid, emoji) {
    return fetch(API, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ post_id: pid, user_id: uid, emoji: emoji }),
    })
      .then(function (r) { return r.json(); })
      .catch(function () { return null; });
  }

  function fetchNames(ids) {
    var uncached = ids.filter(function (id) { return !NAME_CACHE[id]; });
    if (!uncached.length) return Promise.resolve();
    var promises = uncached.map(function (id) {
      return fetch('https://ВАШ_ФОРУМ.mybb.ru/api.php?method=users.get&user_id=' + id)
        .then(function (r) { return r.json(); })
        .then(function (data) {
          if (data && data.response && data.response.users && data.response.users[0] && data.response.users[0].username) {
            NAME_CACHE[id] = data.response.users[0].username;
          }
        })
        .catch(function () {});
    });
    return Promise.all(promises);
  }

  function showTooltip(btn, names) {
    var existing = btn.querySelector('.rx-tooltip');
    if (existing) existing.remove();
    if (!names.length) return;

    var text = names.length <= 3
      ? '\u041F\u0440\u043E\u0440\u0435\u0430\u0433\u0438\u0440\u043E\u0432\u0430\u043B\u0438 ' + names.join(', ') + '.'
      : '\u041F\u0440\u043E\u0440\u0435\u0430\u0433\u0438\u0440\u043E\u0432\u0430\u043B\u0438 ' + names.slice(0, 2).join(', ') + ' \u0438 \u0435\u0449\u0451 ' + (names.length - 2) + '.';

    var tip = document.createElement('div');
    tip.className = 'rx-tooltip';
    tip.textContent = text;
    btn.appendChild(tip);
  }

  function render(pid, counts, mine) {
    counts = normalizeCounts(counts);
    mine = decodeHtml(mine);
    var post = document.getElementById('p' + pid);
    if (!post) return;

    var isOwn = post.dataset.userId && String(post.dataset.userId) === String(uid());

    var box = post.querySelector('.post-box');
    if (!box || box.querySelector('.post-reactions-wrap')) return;

    var t = totalCount(counts);

    var wrap = document.createElement('div');
    wrap.className = 'post-reactions-wrap';

    var hdr = document.createElement('div');
    hdr.className = 'post-reactions-hdr';
    hdr.innerHTML = '<span class="pr-arrow">\u25B6</span>' +
      ' \u0420\u0435\u0430\u043A\u0446\u0438\u0438' +
      ' <span class="pr-count">' + (t > 0 ? '(' + t + ')' : '') + '</span>';

    hdr.addEventListener('click', function () {
      var panel = this.nextElementSibling;
      var isOpen = panel.style.display !== 'none';
      panel.style.display = isOpen ? 'none' : '';
      this.querySelector('.pr-arrow').classList.toggle('open', !isOpen);
    });
    wrap.appendChild(hdr);

    var panel = document.createElement('div');
    panel.className = 'post-reactions';
    panel.style.display = 'none';

    EMOJIS.forEach(function (emoji) {
      var count = counts[emoji] || 0;
      var active = (mine === emoji) ? ' active' : '';

      var btn = document.createElement('span');
      btn.className = 'reaction-btn' + active;
      btn.dataset.post = pid;
      btn.dataset.emoji = decodeHtml(emoji);
      btn.innerHTML = '<span class="emoji">' + emoji + '</span>' +
        '<span class="count">' + count + '</span>';
      if (!isOwn) btn.addEventListener('click', onClick);

      btn.addEventListener('mouseenter', function () {
        var self = this;
        fetch(API + '?post_id=' + pid + '&emoji=' + encodeURIComponent(decodeHtml(emoji)))
          .then(function (r) { return r.json(); })
          .then(function (voterIds) {
            if (!voterIds || !voterIds.length) return;
            fetchNames(voterIds).then(function () {
              var names = voterIds.map(function (id) { return NAME_CACHE[id] || id; });
              showTooltip(self, names);
            });
          })
          .catch(function () {});
      });

      btn.addEventListener('mouseleave', function () {
        var tip = this.querySelector('.rx-tooltip');
        if (tip) tip.remove();
      });

      panel.appendChild(btn);
    });

    wrap.appendChild(panel);
    box.appendChild(wrap);
  }

  function updateUI(pid, counts, mine) {
    counts = normalizeCounts(counts);
    mine = decodeHtml(mine);
    var wrap = document.querySelector('#p' + pid + ' .post-reactions-wrap');
    if (!wrap) return;

    var panel = wrap.querySelector('.post-reactions');
    if (panel) {
      panel.querySelectorAll('.reaction-btn').forEach(function (btn) {
        var e = btn.dataset.emoji;
        btn.querySelector('.count').textContent = counts[e] || 0;
        btn.classList.toggle('active', mine === e);
      });
    }

    var hdr = wrap.querySelector('.post-reactions-hdr');
    if (hdr) {
      var t = totalCount(counts);
      hdr.querySelector('.pr-count').textContent = t > 0 ? '(' + t + ')' : '';
    }
  }

  function onClick(e) {
    var btn = e.currentTarget;
    var pid = btn.dataset.post;
    var emoji = btn.dataset.emoji;
    var userId = uid();

    if (isGuest() || !userId) return;

    var post = document.getElementById('p' + pid);
    if (post && post.dataset.userId && String(post.dataset.userId) === String(userId)) return;

    if (!SEQ[pid]) SEQ[pid] = 0;
    var seq = ++SEQ[pid];
    var prevEmoji = TRACK[pid] || null;
    TRACK[pid] = emoji;
    btn.classList.add('loading');

    var prevReaction = CACHE[pid] || prevEmoji || null;
    var newCounts = optimisticUpdate(pid, emoji, prevReaction);
    updateUI(pid, newCounts, emoji);
    btn.classList.remove('loading');

    var onDone = function (data) {
      if (SEQ[pid] !== seq) return;
      TRACK[pid] = false;
      CACHE[pid] = (data && data.user_reaction) || null;
      updateUI(pid, (data && data.counts) || {}, (data && data.user_reaction) || null);
    };

    toggle(pid, userId, emoji)
      .then(onDone)
      .catch(function () {
        fetch(API + '?post_ids=' + pid + '&user_id=' + userId)
          .then(function (r) { return r.json(); })
          .then(function (data) {
            if (data && data[pid]) onDone(data[pid]);
          });
      });
  }

  function optimisticUpdate(pid, newEmoji, prevEmoji) {
    var panel = document.querySelector('#p' + pid + ' .post-reactions');
    if (!panel) return {};

    var counts = {};
    panel.querySelectorAll('.reaction-btn').forEach(function (btn) {
      counts[btn.dataset.emoji] = parseInt(btn.querySelector('.count').textContent) || 0;
    });

    if (prevEmoji && counts[prevEmoji] > 0) {
      counts[prevEmoji]--;
    }
    if (newEmoji !== prevEmoji) {
      counts[newEmoji] = (counts[newEmoji] || 0) + 1;
    }
    return counts;
  }

  function init() {
    if (!document.getElementById('pun-viewtopic')) return;

    var ids = postIds();
    if (!ids.length) return;

    var userId = uid();

    var newIds = ids.filter(function (id) {
      return !document.querySelector('#p' + id + ' .post-reactions-wrap');
    });
    if (!newIds.length) return;

    fetchAll(newIds, userId).then(function (data) {
      newIds.forEach(function (pid) {
        var info = data[pid] || { counts: {}, user_reaction: null };
        CACHE[pid] = info.user_reaction;
        render(pid, info.counts, info.user_reaction);
      });
    });
  }

  injectCSS();

  if (typeof $ !== 'undefined') {
    $(document).on('pun_mainReady', init);
    $(document).on('pun_post', function () { setTimeout(init, 0); });
    $(document).on('ajaxSuccess', function () { setTimeout(init, 0); });
  }

  var observer = new MutationObserver(function () {
    if (!document.getElementById('pun-viewtopic')) return;
    var unprocessed = document.querySelectorAll('.post[id^="p"]:not(.rx-init)');
    if (!unprocessed.length) return;
    unprocessed.forEach(function (el) { el.classList.add('rx-init'); });
    setTimeout(init, 0);
  });
  if (document.body) {
    observer.observe(document.body, { childList: true, subtree: true });
  } else {
    document.addEventListener('DOMContentLoaded', function () {
      observer.observe(document.body, { childList: true, subtree: true });
    });
  }
})();
</script>
Минифицированная версия (скопируйте целиком в HTML-верх):
Код:
<script>
!function(){"use strict";var e="https://functions.yandexcloud.net/ID_ВАШЕЙ_ФУНКЦИИ",t=["👍","👎","❤️","😂","😮","😢","😡","🔥","🎯","💡"],n={},r={},o={};function a(e){return e?e.replace(/&#(\d+);/g,function(e,t){return String.fromCodePoint(parseInt(t))}):e||""}function i(e){var t={};for(var n in e){var r=a(n);t[r]=(t[r]||0)+e[n]}return t}t=t.map(function(e){return a(e)});var c={};function s(){var e;return"undefined"!=typeof FORUM&&FORUM.get&&(e=FORUM.get("UserID")),e||"undefined"==typeof UserID||(e=UserID),e||0}function u(e){var n=0;return t.forEach(function(t){n+=e[a(t)]||0}),n}function d(n,r,o){r=i(r),o=a(o);var d=document.getElementById("p"+n);if(d){var p=d.dataset.userId&&String(d.dataset.userId)===String(s()),f=d.querySelector(".post-box");if(f&&!f.querySelector(".post-reactions-wrap")){var m=u(r),h=document.createElement("div");h.className="post-reactions-wrap";var b=document.createElement("div");b.className="post-reactions-hdr",b.innerHTML='<span class="pr-arrow">▶</span> Реакции <span class="pr-count">'+(m>0?"("+m+")":"")+"</span>",b.addEventListener("click",function(){var e=this.nextElementSibling,t="none"!==e.style.display;e.style.display=t?"none":"",this.querySelector(".pr-arrow").classList.toggle("open",!t)}),h.appendChild(b);var v=document.createElement("div");v.className="post-reactions",v.style.display="none",t.forEach(function(t){var i=r[t]||0,s=o===t?" active":"",u=document.createElement("span");u.className="reaction-btn"+s,u.dataset.post=n,u.dataset.emoji=a(t),u.innerHTML='<span class="emoji">'+t+'</span><span class="count">'+i+"</span>",p||u.addEventListener("click",l),u.addEventListener("mouseenter",function(){var r=this;fetch(e+"?post_id="+n+"&emoji="+encodeURIComponent(a(t))).then(function(e){return e.json()}).then(function(e){e&&e.length&&function(e){var t=e.filter(function(e){return!c[e]});if(!t.length)return Promise.resolve();var n=t.map(function(e){return fetch("https://ВАШ_ФОРУМ.mybb.ru/api.php?method=users.get&user_id="+e).then(function(e){return e.json()}).then(function(t){t&&t.response&&t.response.users&&t.response.users[0]&&t.response.users[0].username&&(c[e]=t.response.users[0].username)}).catch(function(){})});return Promise.all(n)}(e).then(function(){var t=e.map(function(e){return c[e]||e});!function(e,t){var n=e.querySelector(".rx-tooltip");if(n&&n.remove(),t.length){var r=t.length<=3?"Прореагировали "+t.join(", ")+".":"Прореагировали "+t.slice(0,2).join(", ")+" и ещё "+(t.length-2)+".",o=document.createElement("div");o.className="rx-tooltip",o.textContent=r,e.appendChild(o)}}(r,t)})}).catch(function(){})}),u.addEventListener("mouseleave",function(){var e=this.querySelector(".rx-tooltip");e&&e.remove()}),v.appendChild(u)}),h.appendChild(v),f.appendChild(h)}}}function p(e,t,n){t=i(t),n=a(n);var r=document.querySelector("#p"+e+" .post-reactions-wrap");if(r){var o=r.querySelector(".post-reactions");o&&o.querySelectorAll(".reaction-btn").forEach(function(e){var r=e.dataset.emoji;e.querySelector(".count").textContent=t[r]||0,e.classList.toggle("active",n===r)});var c=r.querySelector(".post-reactions-hdr");if(c){var s=u(t);c.querySelector(".pr-count").textContent=s>0?"("+s+")":""}}}function l(t){var a=t.currentTarget,i=a.dataset.post,c=a.dataset.emoji,u=s();if("undefined"!=typeof FORUM&&FORUM.get&&(d=FORUM.get("GroupID")),d||"undefined"==typeof GroupID||(d=GroupID),"3"!==String(d||3)&&u){var d,l=document.getElementById("p"+i);if(!l||!l.dataset.userId||String(l.dataset.userId)!==String(u)){r[i]||(r[i]=0);var f=++r[i],m=o[i]||null;o[i]=c,a.classList.add("loading");var h=n[i]||m||null,b=function(e,t,n){var r=document.querySelector("#p"+e+" .post-reactions");if(!r)return{};var o={};r.querySelectorAll(".reaction-btn").forEach(function(e){o[e.dataset.emoji]=parseInt(e.querySelector(".count").textContent)||0}),n&&o[n]>0&&o[n]--;t!==n&&(o[t]=(o[t]||0)+1);return o}(i,c,h);p(i,b,c),a.classList.remove("loading");var v=function(e){r[i]===f&&(o[i]=!1,n[i]=e&&e.user_reaction||null,p(i,e&&e.counts||{},e&&e.user_reaction||null))};(function(t,n,r){return fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({post_id:t,user_id:n,emoji:r})}).then(function(e){return e.json()}).catch(function(){return null})})(i,u,c).then(v).catch(function(){fetch(e+"?post_ids="+i+"&user_id="+u).then(function(e){return e.json()}).then(function(e){e&&e[i]&&v(e[i])})})}}}function f(){if(document.getElementById("pun-viewtopic")){var t=Array.from(document.querySelectorAll('.post[id^="p"]')).map(function(e){return e.id.slice(1)});if(t.length){var r=s(),o=t.filter(function(e){return!document.querySelector("#p"+e+" .post-reactions-wrap")});o.length&&function(t,n){return t.length?fetch(e+"?post_ids="+t.join(",")+"&user_id="+n).then(function(e){return e.json()}).catch(function(){return{}}):Promise.resolve({})}(o,r).then(function(e){o.forEach(function(t){var r=e[t]||{counts:{},user_reaction:null};n[t]=r.user_reaction,d(t,r.counts,r.user_reaction)})})}}}!function(){if(!document.getElementById("rx-css")){var e=document.createElement("style");e.id="rx-css",e.textContent=".post-reactions-wrap{margin-top:8px}.post-reactions-hdr{display:inline-flex;align-items:center;gap:4px;padding:3px 10px;border:1px solid #c5d0e6;border-radius:6px;cursor:pointer;font-size:12px;color:#426B9A;background:#f5f5ff;transition:background .15s;user-select:none}.post-reactions-hdr:hover{background:#e0d8f2}.pr-arrow{font-size:10px;transition:transform .2s;display:inline-block}.pr-arrow.open{transform:rotate(90deg)}.pr-count{color:#888}.post-reactions{display:flex;flex-wrap:wrap;gap:3px;padding:5px 7px;margin-top:5px;border:1px solid #c5d0e6;border-radius:8px;background:#f8f8ff;align-items:center}.reaction-btn{display:inline-flex;align-items:center;gap:3px;padding:2px 7px;border:1px solid #d1d1e1;border-radius:5px;cursor:pointer;font-size:13px;line-height:22px;background:#f5f5ff;transition:background .15s;user-select:none;position:relative}.reaction-btn:hover{background:#e0d8f2;border-color:#a0b0d0}.reaction-btn.active{background:#3e779d;border-color:#3e779d;color:#fff}.reaction-btn.loading{opacity:.5;cursor:wait}.reaction-btn .emoji{font-size:15px}.reaction-btn .count{font-size:11px;font-weight:bold;min-width:10px;text-align:center}body.dark-theme .post-reactions-hdr{background:#2d3748;border-color:#4a5b70;color:#7a9cc6}body.dark-theme .post-reactions-hdr:hover{background:#3a4a5e}body.dark-theme .post-reactions{border-color:#4a5b70;background:#262f40}body.dark-theme .reaction-btn{background:#2d3748;border-color:#4a5b70;color:#e2e8f0}body.dark-theme .reaction-btn:hover{background:#3a4a5e;border-color:#5a6b80}body.dark-theme .reaction-btn.active{background:#426B9A;border-color:#426B9A;color:#fff}body.dark-theme .pr-count{color:#8899aa}.rx-tooltip{position:absolute;bottom:100%;left:50%;transform:translateX(-50%);background:#333;color:#fff;padding:4px 8px;border-radius:4px;font-size:11px;white-space:nowrap;z-index:999;pointer-events:none;margin-bottom:4px}body.dark-theme .rx-tooltip{background:#1a1a2e}",document.head.appendChild(e)}}(),"undefined"!=typeof $&&($(document).on("pun_mainReady",f),$(document).on("pun_post",function(){setTimeout(f,0)}),$(document).on("ajaxSuccess",function(){setTimeout(f,0)}));var m=new MutationObserver(function(){if(document.getElementById("pun-viewtopic")){var e=document.querySelectorAll('.post[id^="p"]:not(.rx-init)');e.length&&(e.forEach(function(e){e.classList.add("rx-init")}),setTimeout(f,0))}});document.body?m.observe(document.body,{childList:!0,subtree:!0}):document.addEventListener("DOMContentLoaded",function(){m.observe(document.body,{childList:!0,subtree:!0})})}();
</script>

Можно  также  скрипт в минифицированном виде  сохранить в файл и вставлять его   в HTML-верх форума (Администрирование → Формы → HTML верх) так:

<!-- РЕАКЦИИ на форуме -->
<script src="https://forumstatic.ru/ваш_файл"></script>


8. Результат

После установки:

  • Под каждым постом появляется кнопка ▶ Реакции

  • Если на пост уже есть реакции — рядом показывается их количество

  • Клик на кнопку открывает панель с 10 эмодзи

  • Клик на эмодзи — ставит реакцию (или убирает, если повторно)

  • Счётчики обновляются без перезагрузки страницы

  • Работает тёмная тема форума

  • Гости видят заголовок и счётчики, но кликнуть не могут

  • При наведении на кнопку — тултип с именами проголосовавших


9. Возможные проблемы

Виджет не появляется

  • Проверьте, что скрипт вставлен в HTML-верх (а не в низ)

  • Убедитесь, что API указывает на ваш Worker

  • Откройте F12 → Console — там должна быть информация об ошибках

Реакции не сохраняются

  • Проверьте, что Worker задеплоен: wrangler deploy

  • Проверьте API: curl --

Ошибка CORS

  • Убедитесь, что Worker возвращает заголовок Access-Control-Allow-Origin: *. В коде выше он есть.


10. Набор реакций

Список эмодзи в реакциях

Код

Эмодзи

Смысл

u{1F44D}

👍

Нравится

u{1F44E}

👎

Не нравится

u2764uFE0F

❤️

Супер

u{1F602}

😂

Смешно

u{1F62E}

😮

Ничего себе

u{1F622}

😢

Грустно

u{1F621}

😡

Бесит

u{1F525}

🔥

Огонь

u{1F3AF}

🎯

В точку

u{1F4A1}

💡

Идея


11. Вопросы безопасности.

Мы используем бесплатные лимиты, но плохие парни могут попытаться их нам "высадить" и опустошить. По умолчанию мы все друзья, но надо понимать все риски и противодействия хакерским приёмам.

1. Как можно защитить лимиты Cloudflare Durable Objects: сделать общий Secret на Yandex Cloud и Cloudflare Workers. То, что придёт на Cloudflare без Secret, не будет принято и не уменьшит квоту. Уровень опасности: в данном случае некритичный, т.к. максимум, что грозит на бесплатном лимите Cloudflare - приостановка работы.
2. Как защититься от входящего спама на Yandex Cloud: в принципе никак, и так как на Яндексе у вас будет привязана ваша карта с деньгами, то крайне рекомендуется установить в Yandex Cloud небольшой лимит месячных расходов, например в 50 рублей.

Приятного общения! :)

Отредактировано Merlin777 (Вс, 28 Июн 2026 04:22:29)

+3


Вы здесь » Единый форум поддержки » Новые возможности форумов » Запросы по скриптам #11