Список пользователей

1
Админ
Постов: 123
2
Элита
Постов: 34
3
Элита
Постов: 28
4
VIP
Постов: 26
5
Проверенные
Постов: 25
6
Дизайнер
Постов: 25
7
Пользователи
Постов: 25
8
Пользователи
Постов: 24

  • Страница 1 из 1
  • 1
История Репутации и Отзывы (без API) UcOz JS
Дата: Пятница, 28.11.2025, 11:37 | Сообщение # 1 | | Написал: Начинающий
Автор темы
Мурчанн не в сети
        Сообщений:123
         Регистрация:20.10.2016

Полноценная система отзывов без серверного API

1. Никаких модных бэкендов, никаких REST, GraphQL и прочих гипер-умных слов.
Работает тупо через стандартные юкоз-эндпоинты - как дедовский АК-47: старый, но е.....шит.

2. Отображение репутации с подсчётом



Показывает:

1. текущую репутацию пользователя,

2. ссылку на её историю,

и рядом кнопочку «изменить», которая выглядит как часть дизайна, а не как HTML-сирота.

Кеширование аватарок в памяти

Один раз загрузил аву пользователя → всё, она живёт в кеше как у бабушки в серванте, только не пылится.
Используется быстрый объектный кеш REP._avatars.

Сохранение аватаров изображений в IndexedDB

Да-да, ты не ослышался:

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

Постраничная подгрузка отзывов

Есть своя пагинация:

Кнопка «Загрузить ещё» качает новые комменты без перезапуска модуля, просто увеличивает _page++ и догружает контент следующей страницы, как шаурму слоями.

Локальное добавление отзывов без перезагрузки страницы

После отправки формы:

Отзыв сразу появляется на странице с анимацией, помечается настроением (плюс/минус/0),

получает ник Вы,

дату Только что,

и через секунду обновляется - чтобы подтянуть настоящий ник и аву с профиля.

Красивое всплывающее уведомление "успеха"

Когда отзыв добавлен вылезает аккуратный попап:

Ваш отзыв успешно добавлен!

1. Появляется плавно,

2. уезжает как ниндзя,

3. не ломает страницу,

и не пугает UX как жуткий alert.

Кнопки и блоки легко стилизуются под твой сайт

Не надо лезть в логику, чтобы:

1. поменять цвет,

2. форму,

3. отступ,

4. фон,

или сделать hover-эффект из спилберга.

Работает стабильно и не спамит сервер

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

Общее резюме

Этот модуль:

1. Работает без API

2. быстрее, чем стандартные юкоз отзывы

3. хранит аватарки в IndexedDB

4. умеет подгрузку отзывов по страницам

5. показывает попап-успеха

6. легко стилизуется под любой дизайн

7 хранит всё в кеше и не спамит сервер

Код
<script src="https://bro.usite.pro/js/rep.js"></script>.


JS

Код
// === REP.js: прокачанный комментарийный модуль ===
var REP = {
  settings: {
    noavatar: "/.s/img/icon/social/noavatar.png",
    guest: "Гость"
  },

  _id: 0,
  _group: 0,
  _page: 1,
  _items: [],
  _avatars: {}, // кеш аватарок по href

  // === IndexedDB для стабильного хранения аватарок ===
  dbName: 'REP_DB',
  db: null,

  initDB: function() {
    let request = indexedDB.open(this.dbName, 1);
    request.onupgradeneeded = function(e) {
      let db = e.target.result;
      if(!db.objectStoreNames.contains('avatars')) {
        db.createObjectStore('avatars', { keyPath: 'href' });
      }
    };
    request.onsuccess = e => { this.db = e.target.result; };
    request.onerror = e => console.warn("IndexedDB error", e);
  },

  saveAvatar: function(href, url) {
    if(!this.db) return;
    let tx = this.db.transaction('avatars', 'readwrite');
    let store = tx.objectStore('avatars');
    store.put({ href, url });
  },

  loadAvatarFromDB: function(href, callback) {
    if(!this.db) { callback(null); return; }
    let tx = this.db.transaction('avatars', 'readonly');
    let store = tx.objectStore('avatars');
    let req = store.get(href);
    req.onsuccess = function(){ callback(req.result ? req.result.url : null); };
    req.onerror = function(){ callback(null); };
  },

  // --- Загрузка комментариев ---
  upload: function() {
    var self = this;
    $.get('/index/9-' + self._id + '-' + self._page)
      .done(function(all) {
        var $resp = $(all);
        var content = $resp.find('cmd[p=content]').html();
        if(!content || !content.trim()){
          if(self._page===1) $('.rep-do-list').html('Отзывы отсутствуют.');
          return;
        }
        var $html = $(content);
        if(self._page===1) self._items = [];

        $html.find('[id^=blr]').each(function() {
          var idMatch = $(this).attr('id').match(/\d+/);
          var blr = idMatch ? idMatch[0] : Math.floor(Math.random()*1e6);

          var $userLink = $(this).find('.banHUser');
          var href = $userLink.attr('href') || '/';
          var username = ($userLink.find('b').text() || $userLink.text() || self.settings.guest).trim();

          var dateText = $(this).find('.flex-justify-between span:last').text().trim() ||
                         $(this).find('.prj-date').text().trim() ||
                         $(this).find('td[width="70%"] + td').text().trim() || "—";

          var rateTitle = $(this).find('[title^="Уровень"]').attr('title') || "";
          var rate = rateTitle.match(/[+-]\d+/) ? rateTitle.match(/[+-]\d+/)[0] : "0";

          var answer = $('#mmtx' + blr, this).text().trim() ||
                       $(this).find('p.text').text().trim() || "";

          self._items.push({blr, href, username, date: dateText, rate, answer});
        });

        self.render();
      })
      .fail(function() {
        console.warn('Ошибка загрузки отзывов');
        if(REP._items.length===0) $('.rep-do-list').html('Ошибка загрузки отзывов.');
      });
  },

  // --- Рендер комментариев ---
  render: function() {
    var self = this;
    var $list = $('.rep-do-list');
    $list.html('');

    if(!this._items.length) { $list.html('Отзывы отсутствуют.'); return; }

    this._items.forEach(function(it){
      var icon = '<span class="rep-icon rep-neutral">0</span>';
      var cls = 'rep-neutral';
      if(it.rate.indexOf('+')!==-1){ icon='<span class="rep-icon rep-pos">+</span>'; cls='rep-pos'; }
      if(it.rate.indexOf('-')!==-1){ icon='<span class="rep-icon rep-neg">−</span>'; cls='rep-neg'; }

      var safeAnswer = $('<div>').text(it.answer).html();
      var safeName = $('<div>').text(it.username).html();
      var safeRate = $('<div>').text(it.rate).html();

      var ava = self._avatars[it.href] || self.settings.noavatar;

      $list.append(
        '<div class="answer '+cls+'" id="blr'+it.blr+'">'+
          '<div class="avatar-b"><img src="'+ava+'" alt="" /></div>'+
          '<div class="text-b">'+
            '<div>'+icon+' <span class="user-name"><a href="'+it.href+'">'+safeName+'</a></span> '+
            '<span class="prj-date">'+it.date+'</span> '+
            ' Оценка: <span class="rating-val">'+safeRate+'</span></div>'+
            '<p class="text">'+safeAnswer+'</p>'+
          '</div>'+
        '</div>'
      );
    });

    // Запуск подгрузки аватарок
    setTimeout(this.loadAvatars.bind(this), 200);
  },

  // --- Подгрузка аватарок из профиля ---
  loadAvatars: function(){
    var self = this;
    $(".rep-do-list .answer").each(function(){
      var $card = $(this);
      var $link = $card.find(".user-name a");
      if(!$link.length) return;

      var href = $link.attr("href");
      if(self._avatars[href]) return;

      // сначала пробуем IndexedDB
      self.loadAvatarFromDB(href, function(url){
        if(url){
          self._avatars[href] = url;
          $card.find(".avatar-b img").attr("src", url);
        } else {
          // если нет в DB, грузим с профиля
          $.get(href).done(function(resp){
            var ava = null;
            var m = resp.match(/<div[^>]+class=["']profile-photo["'][^>]*style=["'][^"']*background-image\s*:\s*url\((['"]?)([^'")]+)\1\)/i);
            if(m && m[2]) ava = m[2].trim();
            if(!ava) ava = self.settings.noavatar;
            self._avatars[href] = ava;
            $card.find(".avatar-b img").attr("src", ava);
            self.saveAvatar(href, ava);
          }).fail(function(){
            $card.find(".avatar-b img").attr("src", self.settings.noavatar);
          });
        }
      });
    });
  },

  // --- Добавление локального отзыва ---
  addLocal: function(text, act, authorName){
    var blr = Math.floor(Math.random()*1e6);
    var username = authorName || 'Вы';
    var rate = act===2?'+1':(act===1?'-1':'0');
    var date = 'Только что';

    this._items.unshift({blr, href:'/', username, date, rate, answer:text});
    this.render();

    // Скрытая перезагрузка комментариев для подтягивания настоящего ник/аватар
    setTimeout(() => this.upload(), 1000);

    // Всплывающее окно успешного добавления
    showSuccessPopup("Ваш отзыв успешно добавлен! Всё прошло замечательно 🙂");
  }
};

// === Красивое всплывающее окно ===
function showSuccessPopup(message){
  let popup = document.createElement("div");
  popup.className = "rep-success-popup";
  popup.innerHTML = '<span class="icon">✅</span> ' + message;
  document.body.appendChild(popup);
  setTimeout(()=> popup.classList.add("show"), 10);
  setTimeout(()=>{
    popup.classList.remove("show");
    setTimeout(()=> document.body.removeChild(popup), 500);
  }, 3000);
}

const style = document.createElement("style");
style.innerHTML = `
.rep-success-popup{
  position: fixed; top:20px; right:20px;
  background:#0078d4; color:#fff;
  padding:14px 20px; border-radius:8px;
  font-family: 'Inter', sans-serif; font-size:15px;
  box-shadow:0 4px 12px rgba(0,0,0,0.2);
  opacity:0; transform:translateY(-20px);
  display:flex; align-items:center; gap:10px;
  z-index:9999; transition: all 0.4s ease;
}
.rep-success-popup.show{ opacity:1; transform:translateY(0);}
.rep-success-popup .icon{ font-size:18px;}
`;
document.head.appendChild(style);

// === Инициализация ===
$(function(){
  REP.initDB();
  REP._id = $('.rep-do-id').val();
  REP._group = $('.rep-do-group').val();

  REP.upload();

  // Кнопка "Загрузить ещё"
  $('.rep-do-add').off('click.rep').on('click.rep', function(e){
    e.preventDefault();
    REP._page++;
    REP.upload();
  });

  // Отправка формы
  $('.rep-do-go').off('click.rep').on('click.rep', function(e){
    e.preventDefault();
    var reason = $('.rep-do-reason').val().trim();
    var act = parseInt($('input[name="rep"]:checked').val());
    if(!reason){ alert('Введите текст'); return; }
    if(isNaN(act)){ alert('Выберите оценку'); return; }

    $.get('/index/14').done(function(u){
      $.post('/index/', {
        a:'23', t:'1', s: $('.rep-do-id').val(),
        reason: reason, act: act,
        ssid: $("input[name='ssid']", u).val()
      }).done(function(data){
        var answer = $(data).find('cmd[p=innerHTML]').text();
        if(answer.indexOf('myWinLoadSD')!==-1){
          REP.addLocal(reason, act, 'Вы');
          $('.rep-do-reason').val('');
          $('input[name="rep"]').prop('checked', false);
        } else alert('Ошибка отправки');
      }).fail(function(){ alert('Ошибка отправки (network)'); });
    }).fail(function(){ alert('Ошибка получения ssid'); });
  });
});


CSS

Код
.rep-rating {
display: flex;
justify-content: center;
gap: 15px;
margin: -5px 0;
}

.rep-smile {
font-size: 20px;
cursor: pointer;
transition: transform 0.2s, filter 0.2s;
}

.rep-smile:hover {
transform: scale(1.3);
}

.rep-smile.selected {
transform: scale(1.6);
filter: drop-shadow(0 0 6px #488BFA);
}

.rating-btn {
font-size: 28px;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background: linear-gradient(145deg, #f0f0f0, #e0e0e0);
box-shadow: 2px 2px 5px rgba(0,0,0,0.15);
cursor: pointer;
transition: all 0.3s ease;
user-select: none;
}

/* Hover эффект */
.rating-btn:hover {
transform: scale(1.2);
box-shadow: 4px 4px 12px rgba(0,0,0,0.25);
}

/* Активная кнопка */
.rep-rating-modern input:checked + .rating-btn {
transform: scale(1.25);
background: linear-gradient(145deg, #488BFA, #2a6edb);
color: #fff;
box-shadow: 0 0 15px rgba(72,139,250,0.6);
}

/* Разные цвета для каждой оценки */
#rep-like:checked + label { background: #23a455; color: #fff; }
#rep-neutral:checked + label { background: #9aa0a6; color: #fff; }
#rep-dislike:checked + label { background: #c0392b; color: #fff; }

.feedback-form {
max-width: 720px;
margin: 20px auto;
padding: 0;
background: transparent; /* убрали фон */
border: none; /* убрали рамку */
display: flex;
flex-direction: column;
gap: 10px;
}

.feedback-form textarea {
width: 100%;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #ccc;
font-family: 'PT Sans', sans-serif;
font-size: 14px;
resize: vertical;
min-height: 50px; /* меньше по вертикали */
transition: all 0.3s ease;
}

.feedback-form textarea:focus {
border-color: #488BFA;
box-shadow: 0 0 5px rgba(72,139,250,0.5);
outline: none;
}

.rep-rating {
display: flex;
gap: 15px;
font-size: 16px;
}

.rep-rating input {
margin-right: 4px;
cursor: pointer;
}

.btn-primary.rep-do-go {
width: 100%;
padding: 10px;
font-size: 16px;
font-weight: 700;
border-radius: 6px;
border: 2px solid #488BFA;
background-color: #fff;
color: #488BFA;
cursor: pointer;
transition: all 0.3s ease;
}

.btn-primary.rep-do-go:hover {
background-color: #488BFA;
color: #fff;
transform: translateY(-2px);
}

.btn-primary.rep-do-go:active {
background-color: #3367f4;
transform: translateY(0);
}

/* Минимальные стили для иконок и аватаров */
.rep-icon { display:inline-block; width:18px; height:18px; line-height:18px; text-align:center; border-radius:50%; color:#fff; font-weight:700; margin-right:6px; font-size:12px; }
.rep-pos { background:#23a455; } /* зеленый + */
.rep-neutral { background:#9aa0a6; } /* серый 0 */
.rep-neg { background:#c0392b; } /* красный - */

.answer { display:flex; gap:10px; padding:10px; border-radius:8px; margin-bottom:8px; align-items:flex-start; background:#fff; }
.avatar-b img { width:48px; height:48px; border-radius:50%; object-fit:cover; display:block; }
.text-b { flex:1; }
.prj-date { color:#777; margin-left:8px; font-size:12px; }
.rating-val { font-weight:700; margin-left:6px; }

/* Стиль для кнопки "Загрузить еще" */
.rep-do-add {
display: inline-block;
width: 20%; /* на всю ширину контейнера */
font-size: 11px;
font-weight: 700;
padding: 5px 10px;
border-radius: 5px;
border: 1px solid #488BFA; /* синий контур */
background-color: #fff; /* белый фон */
color: #488BFA; /* синий текст */
cursor: pointer;
transition: all 0.3s ease;
margin-left: 590px; /* сдвиг кнопки влево */
}

/* При наведении */
.rep-do-add:hover {
background-color: #488BFA; /* синий фон */
color: #fff; /* белый текст */
transform: translateY(-2px); /* лёгкий подъём */
}

/* При клике */
.rep-do-add:active {
background-color: #3367f4; /* темно-синий */
transform: translateY(0);
}

/* Стили для кнопки input */
.btn-primary.rep-do-go {
display: inline-block;
width: 103%; /* оставляем, как у тебя */
font-size: 12px;
font-weight: 700;
padding: 8px 12px;
border-radius: 5px;
border: 1px solid #488BFA; /* синий контур */
background-color: #fff; /* белый фон */
color: #488BFA; /* синий текст */
cursor: pointer;
transition: all 0.3s ease;
}

/* Наведение */
.btn-primary.rep-do-go:hover {
background-color: #488BFA; /* синий фон */
color: #fff; /* белый текст */
transform: translateY(-2px); /* лёгкий подъём */
}

/* Клик */
.btn-primary.rep-do-go:active {
background-color: #3367f4; /* темно-синий при клике */
transform: translateY(0);
}

/* === Стиль комментариев для REP.js === */
.rep-do-list .answer {
display: flex !important;
padding: 10px !important;
border-radius: 8px !important;
background-color: #f9f9f9 !important;
margin-bottom: 10px !important;
transition: background 0.3s !important;
align-items: flex-start !important;
}

.rep-do-list .answer:hover {
background-color: #eef6ff !important;
}

.rep-do-list .answer .avatar-b img {
width: 50px !important;
height: 50px !important;
border-radius: 50% !important;
border: 2px solid #ddd !important;
margin-right: 10px !important;
}

.rep-do-list .answer .text-b {
flex: 1 !important;
}

.rep-do-list .answer .user-name a {
font-weight: bold !important;
color: #0078d4 !important;
text-decoration: none !important;
}

.rep-do-list .answer .prj-date {
font-size: 12px !important;
color: #888 !important;
margin-left: 5px !important;
}

.rep-do-list .answer .text {
margin: 5px 0 0 0 !important;
line-height: 1.4 !important;
color: #333 !important;
}

.rep-do-list .answer .rating-val {
font-size: 12px !important;
margin-left: 3px !important;
color: green !important;
}

/* Цветовая маркировка по оценке */
.rep-do-list .answer.rep-pos { background-color: #f0fff0 !important; }
.rep-do-list .answer.rep-neg { background-color: #fff0f0 !important; }
.rep-do-list .answer.rep-neutral { background-color: #f9f9f9 !important; }

/* Отступы аватарок и текста */
.rep-do-list .answer .avatar-b {
flex-shrink: 0 !important;
}

.rep-do-list .answer .text-b {
flex: 1 1 auto !important;
}

/* === Всплывающее окно успеха === */
.rep-success-popup {
position: fixed !important;
top: 20px !important;
right: 20px !important;
background: #0078d4 !important;
color: #fff !important;
padding: 14px 20px !important;
border-radius: 8px !important;
font-family: 'Inter', sans-serif !important;
font-size: 15px !important;
box-shadow: 0 4px 12px rgba(0,0,0,0.2) !important;
opacity: 0 !important;
transform: translateY(-20px) !important;
display: flex !important;
align-items: center !important;
gap: 10px !important;
z-index: 9999 !important;
transition: all 0.4s ease !important;
}

.rep-success-popup.show {
opacity: 1 !important;
transform: translateY(0) !important;
}

.rep-success-popup .icon {
font-size: 20px !important;
}


Это сама форма , но вы можете свою сделать.
Исходники все есть ... 2

Код
<div class="rep-do-list"><img src="/.s/img/wd/1/ajax.gif"></div>

<input type="hidden" class="rep-do-id" value="$_USER_ID$">
<input type="hidden" class="rep-do-group" value="$GROUP_ID$">

<div class="feedback-form">

<div class="rep-rating">
<input type="radio" name="rep" value="2" id="rep-good" hidden>
<input type="radio" name="rep" value="0" id="rep-neutral" hidden>
<input type="radio" name="rep" value="1" id="rep-bad" hidden>

<span class="rep-smile" data-val="2">😄</span>
<span class="rep-smile" data-val="0">😐</span>
<span class="rep-smile" data-val="1">😢</span>
</div>

<textarea class="rep-do-reason" name="rtext" rows="3"
placeholder="Напиши свой отзыв..."></textarea>

<input class="btn-primary rep-do-go" type="submit" value="Добавить отзыв">

<button type="button" class="rep-do-add">Загрузить еще</button>

<?if($_REP_READ_URL$)?>

<div class="profile-row" style="border:0px solid #488BFA; border-radius:6px; padding:4px 8px; display:flex; align-items:center; max-width:100px; background:#f9f9f9;">
<div class="profile-row-name" style="font-weight:500; margin-right:8px;">Репутация:</div>
<div class="profile-row-content" style="flex:1; display:flex; align-items:center; justify-content:space-between;">
<span><a href="$_REP_READ_URL$" title="Смотреть историю репутации" style="color:#0078d4; text-decoration:none;">$_REPUTATION$</a></span>
<?if($_REP_DO_URL$)?>
<span class="profile-small" style="margin-left:10px;">
<a href="$_REP_DO_URL$" style="color:#488BFA; font-weight:300; text-decoration:none; padding:4px 5px; border:1px solid #488BFA; border-radius:4px; transition: all 0.3s;">изменить</a>
</span>
<?endif?>
</div>
</div>

<?endif?>

</div>

<script>
const smiles = document.querySelectorAll('.rep-smile');
smiles.forEach(smile => {
smile.addEventListener('click', () => {
// снимаем выделение с других
smiles.forEach(s => s.classList.remove('selected'));
// выделяем кликнутый
smile.classList.add('selected');
// ставим значение в скрытую радиокнопку
const val = smile.dataset.val;
const radio = document.querySelector(`input[name="rep"][value="${val}"]`);
if(radio) radio.checked = true;
});
});
</script>


Ссылка на форму , можно по смотреть

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
Дата: Пятница, 28.11.2025, 11:46 | Сообщение # 2 | | Написал: Начинающий
Автор темы
Мурчанн не в сети
        Сообщений:123
         Регистрация:20.10.2016

Тестируем форму отзывов. 27

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
Дата: Пятница, 28.11.2025, 20:59 | Сообщение # 3 | | Написал: Начинающий
Автор темы
Мурчанн не в сети
        Сообщений:123
         Регистрация:20.10.2016

Обязательно подключите библиотеки jquery!!!
На случай , если работать не будет. .

Код
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>

Мурчанн

Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.
  • Страница 1 из 1
  • 1
Поиск: