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

1
Админ
Постов: 288
2
VIP
Постов: 75
3
Элита
Постов: 62
4
Проверенные
Постов: 46
5
Проверенные
Постов: 45
6
Проверенные
Постов: 39
7
VIP
Постов: 36
8
VIP
Постов: 35

  • Страница 1 из 1
  • 1
История Репутации и Отзывы (без API) UcOz JS
Дата: Пятница, 28.11.2025, 11:37 | Сообщение # 1 | | Написал: Известный
Автор темы
Мурчанн не в сети
        Сообщений:288
         Регистрация: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 | | Написал: Известный
Автор темы
Мурчанн не в сети
        Сообщений:288
         Регистрация:20.10.2016
За уважение окружающих За уважение окружающих За хорошее поведение За преданность форуму За любовь народа

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

Мурчанн

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

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

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

Мурчанн

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