Часто на файлообменниках, форумах и каталогах файлов хочется дать пользователям возможность быстро оценить материал — не звёздочками и не длинной анкетой, а простым «нравится / не нравится». Собрал именно такой виджет: компактный блок с карточкой автора, кнопками 👍/👎 и рейтингом, который обновляется сразу после клика, без перезагрузки страницы.
## Что он умеет
- - **Карточка автора файла** — аватар, имя и ссылка на профиль.
- - **Название и краткое описание** материала со ссылкой на страницу файла.
- - **Кнопки «Хорошо» и «Ужасно»** — по сути лайк и дизлайк.
- - **Итоговый счёт** в виде цифры со знаком: `+3`, `-2`, `0` — сразу видно, нравится людям файл или нет, даже не читая точный процент.
- - **Статистика голосов** — сколько всего человек оценили и какая средняя оценка.
- - **Уведомление внизу экрана** — короткое всплывающее сообщение о результате клика: засчитан голос, уже голосовали или произошла ошибка соединения.
- - Всё это **без перезагрузки страницы** — счётчик и статус меняются на лету через AJAX.
## Как это выглядит для пользователя
1. Человек заходит на страницу файла и видит блок с карточкой автора и текущим рейтингом.
2. Нажимает 👍 или 👎.
3. Пока идёт запрос, кнопки на секунду становятся полупрозрачными — как будто «задумались».
4. Приходит ответ от сервера — и счёт, количество голосов и средняя оценка тут же обновляются, а внизу экрана выскакивает уведомление с результатом.
5. Если человек уже голосовал за этот файл раньше, вместо повторного зачёта голоса он увидит сообщение «Вы уже оценивали этот файл».
6. Если вдруг с соединением проблема, виджет не «зависает» молча, а честно сообщает об ошибке и предлагает попробовать позже.
## Как это устроено технически (в двух словах)
Виджет состоит из трёх частей:
- **Разметка (HTML)** — карточка с автором, кнопками и статистикой. Подставляется автоматически, если использовать его как шаблон для файлов на движке (переменные вроде имени пользователя, названия файла и текущего рейтинга заполняются самим движком).
- **Стили (CSS)** — аккуратный, адаптивный вид блока: на мобильных элементы выстраиваются в столбик, кнопки растягиваются на всю ширину.
- **Скрипт (JavaScript, на jQuery)** — при клике отправляет запрос на сервер, разбирает ответ и обновляет цифры на странице. Написан с расчётом на совместимость со старыми версиями jQuery, которые нередко используются на бесплатных движках сайтов.
Все три части лежат в одном файле — можно скопировать целиком и вставить в шаблон карточки файла.
## Кому пригодится
Подойдёт для любого сайта, где есть файлы, публикации или материалы от разных авторов и хочется дать посетителям простой способ оценить их одним кликом: файлообменники, каталоги, доски объявлений, блоги с загружаемыми материалами.
---
Если будете использовать у себя — под свой движок наверняка придётся немного поправить адрес запроса и формат ответа сервера (в моём случае это связка `a=65`, `mark`, `mod` под конкретную систему рейтинга), но общая логика — карточка, кнопки, живое обновление счёта — останется той же.
Код
<div id="file-rating-$ID$" class="file-rating" data-id="$ID$">
<div class="fr-user">
<a class="fr-avatar" href="$PROFILE_URL$">
<?if($AVATAR_URL$)?>
<img src="$AVATAR_URL$" alt="$USER$">
<?else?>
<img src="/images/noavatar.png" alt="">
<?endif?>
</a>
<div class="fr-user-info">
<a href="$PROFILE_URL$" class="fr-username">$USER$</a>
<div class="fr-label">Автор файла</div>
</div>
</div>
<div class="fr-content">
<div class="fr-title">
<a href="$ENTRY_URL$"><?if(len($ENTRY_TITLE$)>40)?><?substr($ENTRY_TITLE$,0,40)?>..<?else?>$ENTRY_TITLE$<?endif?></a>
</div>
<div class="fr-desc"><?if(len($BRIEF$)>80)?><?substr($BRIEF$,0,80)?>..<?else?>$BRIEF$<?endif?></div>
<div class="fr-rating">
<a href="#" class="fr-btn fr-bad" onclick="return fileRating.vote($ID$,-1,'load')">👎 Ужасно</a>
<div class="fr-result">
<span class="fr-score">
<script>(function(){var t=Math.round($RATING$*$RATED$)-2*$RATED$;document.write(t>0?'+'+t:t);})();</script>
</span>
</div>
<a href="#" class="fr-btn fr-good" onclick="return fileRating.vote($ID$,1,'load')">👍 Хорошо</a>
</div>
<div class="fr-stat">
<span>
Голосов: <b class="fr-votes-count">$RATED$</b>
</span>
<span>
Средняя: <b class="fr-avg-rating">$RATING$</b>/ из 3
</span>
</div>
</div>
</div>
<style>
.file-rating{
display:flex;
align-items:center;
gap:18px;
padding:15px;
background:#fff;
border:1px solid #e5e5e5;
border-radius:14px;
margin:10px 0
}
.fr-user{
display:flex;
align-items:center;
min-width:180px
}
.fr-avatar{
width:55px;
height:55px;
border-radius:50%;
overflow:hidden;
display:block;
margin-right:10px;
background:#eee
}
.fr-avatar img{
width:100%;
height:100%;
object-fit:cover
}
.fr-user-info{
line-height:18px
}
.fr-username{
font-size:14px;
font-weight:700;
color:#333;
text-decoration:none
}
.fr-label{
font-size:11px;
color:#999
}
.fr-content{
flex:1
}
.fr-title a{
font-size:18px;
font-weight:700;
color:#333;
text-decoration:none
}
.fr-desc{
margin-top:5px;
font-size:13px;
color:#777
}
.fr-rating{
display:flex;
align-items:center;
gap:8px;
margin-top:12px
}
.fr-btn{
height:38px;
padding:0 14px;
display:flex;
align-items:center;
justify-content:center;
border:1px solid #ddd;
border-radius:8px;
background:#fff;
color:#555;
font-size:13px;
font-weight:600;
text-decoration:none;
cursor:pointer;
transition:.2s
}
.fr-good:hover{
background:#edfff4;
border-color:#35b76b;
color:#168447
}
.fr-bad:hover{
background:#fff1f1;
border-color:#df6666;
color:#d04444
}
.fr-result{
width:48px;
height:38px;
display:flex;
align-items:center;
justify-content:center;
border-radius:8px;
background:#f2f2f2;
font-size:18px;
font-weight:800;
color:#888
}
.fr-positive{
background:#e9f9ef;
color:#168447
}
.fr-negative{
background:#fff0f0;
color:#d04444
}
.fr-zero{
background:#f2f2f2;
color:#999
}
.fr-stat{
display:flex;
gap:12px;
margin-top:8px;
font-size:12px;
color:#999
}
.fr-stat b{
color:#555
}
.fr-loading{
opacity:.5;
pointer-events:none
}
.fr-voted{
opacity:.75
}
#file-rating-message{
display:none;
position:fixed;
left:50%;
bottom:30px;
transform:translateX(-50%);
padding:12px 20px;
border-radius:10px;
background:#222;
color:#fff;
font-size:14px;
font-weight:600;
z-index:99999
}
@media(max-width:700px){
.file-rating{
display:block
}
.fr-user{
margin-bottom:12px
}
.fr-rating{
flex-wrap:wrap
}
.fr-btn{
flex:1
}
.fr-stat{
flex-wrap:wrap
}
}
</style>
<script>
(function ($) {
'use strict';
window.fileRating = {
vote: function (id, mark, module) {
if (!id || (mark !== 1 && mark !== -1)) {
return false;
}
var box = $('#file-rating-' + id);
if (!box.length) {
return false;
}
var buttons = box.find('.fr-btn');
var result = box.find('.fr-result');
var ratingMark = mark + 2;
buttons.addClass('fr-loading');
$.ajax({
url: '/' + module + '/',
type: 'POST',
dataType: 'text',
data: {
a: '65',
id: id,
mark: ratingMark,
mod: module,
ajax: '2'
},
success: function (response) {
buttons.removeClass('fr-loading');
var data = $('<div>').html(response).text();
var match = data.match(/рейтинг:\s*([\d.]+)\/(\d+)/i);
if (!match) {
if (data.indexOf('уже оценивали') !== -1) {
buttons.addClass('fr-voted');
fileRating.message('Вы уже оценивали этот файл.');
} else {
fileRating.message('Не удалось засчитать оценку.');
}
return;
}
var average = parseFloat(match[1]);
var votes = parseInt(match[2], 10);
var score = Math.round(average * votes) - (2 * votes);
box.find('.fr-score').text(score > 0 ? '+' + score : score);
box.find('.fr-votes-count').text(votes);
box.find('.fr-avg-rating').text(average.toFixed(1));
result
.removeClass('fr-positive fr-negative fr-zero')
.addClass(
score > 0
? 'fr-positive'
: score < 0
? 'fr-negative'
: 'fr-zero'
);
buttons.addClass('fr-voted');
fileRating.message(
mark === 1
? 'Ваш лайк засчитан.'
: 'Ваш дизлайк засчитан.'
);
},
error: function () {
buttons.removeClass('fr-loading');
fileRating.message('Ошибка соединения. Попробуйте позже.');
}
});
return false;
},
message: function (text) {
var msg = $('#file-rating-message');
if (!msg.length) {
$('body').append('<div id="file-rating-message"></div>');
msg = $('#file-rating-message');
}
msg.stop(true, true)
.text(text)
.fadeIn(200);
clearTimeout(window.fileRatingMessageTimer);
window.fileRatingMessageTimer = setTimeout(function () {
msg.fadeOut(300);
}, 2500);
}
};
})(jQuery);
</script>
<div class="fr-user">
<a class="fr-avatar" href="$PROFILE_URL$">
<?if($AVATAR_URL$)?>
<img src="$AVATAR_URL$" alt="$USER$">
<?else?>
<img src="/images/noavatar.png" alt="">
<?endif?>
</a>
<div class="fr-user-info">
<a href="$PROFILE_URL$" class="fr-username">$USER$</a>
<div class="fr-label">Автор файла</div>
</div>
</div>
<div class="fr-content">
<div class="fr-title">
<a href="$ENTRY_URL$"><?if(len($ENTRY_TITLE$)>40)?><?substr($ENTRY_TITLE$,0,40)?>..<?else?>$ENTRY_TITLE$<?endif?></a>
</div>
<div class="fr-desc"><?if(len($BRIEF$)>80)?><?substr($BRIEF$,0,80)?>..<?else?>$BRIEF$<?endif?></div>
<div class="fr-rating">
<a href="#" class="fr-btn fr-bad" onclick="return fileRating.vote($ID$,-1,'load')">👎 Ужасно</a>
<div class="fr-result">
<span class="fr-score">
<script>(function(){var t=Math.round($RATING$*$RATED$)-2*$RATED$;document.write(t>0?'+'+t:t);})();</script>
</span>
</div>
<a href="#" class="fr-btn fr-good" onclick="return fileRating.vote($ID$,1,'load')">👍 Хорошо</a>
</div>
<div class="fr-stat">
<span>
Голосов: <b class="fr-votes-count">$RATED$</b>
</span>
<span>
Средняя: <b class="fr-avg-rating">$RATING$</b>/ из 3
</span>
</div>
</div>
</div>
<style>
.file-rating{
display:flex;
align-items:center;
gap:18px;
padding:15px;
background:#fff;
border:1px solid #e5e5e5;
border-radius:14px;
margin:10px 0
}
.fr-user{
display:flex;
align-items:center;
min-width:180px
}
.fr-avatar{
width:55px;
height:55px;
border-radius:50%;
overflow:hidden;
display:block;
margin-right:10px;
background:#eee
}
.fr-avatar img{
width:100%;
height:100%;
object-fit:cover
}
.fr-user-info{
line-height:18px
}
.fr-username{
font-size:14px;
font-weight:700;
color:#333;
text-decoration:none
}
.fr-label{
font-size:11px;
color:#999
}
.fr-content{
flex:1
}
.fr-title a{
font-size:18px;
font-weight:700;
color:#333;
text-decoration:none
}
.fr-desc{
margin-top:5px;
font-size:13px;
color:#777
}
.fr-rating{
display:flex;
align-items:center;
gap:8px;
margin-top:12px
}
.fr-btn{
height:38px;
padding:0 14px;
display:flex;
align-items:center;
justify-content:center;
border:1px solid #ddd;
border-radius:8px;
background:#fff;
color:#555;
font-size:13px;
font-weight:600;
text-decoration:none;
cursor:pointer;
transition:.2s
}
.fr-good:hover{
background:#edfff4;
border-color:#35b76b;
color:#168447
}
.fr-bad:hover{
background:#fff1f1;
border-color:#df6666;
color:#d04444
}
.fr-result{
width:48px;
height:38px;
display:flex;
align-items:center;
justify-content:center;
border-radius:8px;
background:#f2f2f2;
font-size:18px;
font-weight:800;
color:#888
}
.fr-positive{
background:#e9f9ef;
color:#168447
}
.fr-negative{
background:#fff0f0;
color:#d04444
}
.fr-zero{
background:#f2f2f2;
color:#999
}
.fr-stat{
display:flex;
gap:12px;
margin-top:8px;
font-size:12px;
color:#999
}
.fr-stat b{
color:#555
}
.fr-loading{
opacity:.5;
pointer-events:none
}
.fr-voted{
opacity:.75
}
#file-rating-message{
display:none;
position:fixed;
left:50%;
bottom:30px;
transform:translateX(-50%);
padding:12px 20px;
border-radius:10px;
background:#222;
color:#fff;
font-size:14px;
font-weight:600;
z-index:99999
}
@media(max-width:700px){
.file-rating{
display:block
}
.fr-user{
margin-bottom:12px
}
.fr-rating{
flex-wrap:wrap
}
.fr-btn{
flex:1
}
.fr-stat{
flex-wrap:wrap
}
}
</style>
<script>
(function ($) {
'use strict';
window.fileRating = {
vote: function (id, mark, module) {
if (!id || (mark !== 1 && mark !== -1)) {
return false;
}
var box = $('#file-rating-' + id);
if (!box.length) {
return false;
}
var buttons = box.find('.fr-btn');
var result = box.find('.fr-result');
var ratingMark = mark + 2;
buttons.addClass('fr-loading');
$.ajax({
url: '/' + module + '/',
type: 'POST',
dataType: 'text',
data: {
a: '65',
id: id,
mark: ratingMark,
mod: module,
ajax: '2'
},
success: function (response) {
buttons.removeClass('fr-loading');
var data = $('<div>').html(response).text();
var match = data.match(/рейтинг:\s*([\d.]+)\/(\d+)/i);
if (!match) {
if (data.indexOf('уже оценивали') !== -1) {
buttons.addClass('fr-voted');
fileRating.message('Вы уже оценивали этот файл.');
} else {
fileRating.message('Не удалось засчитать оценку.');
}
return;
}
var average = parseFloat(match[1]);
var votes = parseInt(match[2], 10);
var score = Math.round(average * votes) - (2 * votes);
box.find('.fr-score').text(score > 0 ? '+' + score : score);
box.find('.fr-votes-count').text(votes);
box.find('.fr-avg-rating').text(average.toFixed(1));
result
.removeClass('fr-positive fr-negative fr-zero')
.addClass(
score > 0
? 'fr-positive'
: score < 0
? 'fr-negative'
: 'fr-zero'
);
buttons.addClass('fr-voted');
fileRating.message(
mark === 1
? 'Ваш лайк засчитан.'
: 'Ваш дизлайк засчитан.'
);
},
error: function () {
buttons.removeClass('fr-loading');
fileRating.message('Ошибка соединения. Попробуйте позже.');
}
});
return false;
},
message: function (text) {
var msg = $('#file-rating-message');
if (!msg.length) {
$('body').append('<div id="file-rating-message"></div>');
msg = $('#file-rating-message');
}
msg.stop(true, true)
.text(text)
.fadeIn(200);
clearTimeout(window.fileRatingMessageTimer);
window.fileRatingMessageTimer = setTimeout(function () {
msg.fadeOut(300);
}, 2500);
}
};
})(jQuery);
</script>

0 комментариев
Присоединиться к обсуждению
Поделитесь мнением об этом материале — комментарий появится в списке слева сразу после отправки.