Дата: Пятница, 28.11.2025, 11:37 | Сообщение # 1 |
|
Написал: Начинающий
Автор темы
Мурчанн
не в сети
Сообщений: 123
Полноценная система отзывов без серверного 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; }
Это сама форма , но вы можете свою сделать. Исходники все есть ... Код
<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, 20:59 | Сообщение # 3 |
|
Написал: Начинающий
Автор темы
Мурчанн
не в сети
Сообщений: 123
Обязательно подключите библиотеки jquery!!! На случай , если работать не будет. . Код
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
Признаюсь, не знаю почему, но глядя на звезды мне всегда хочется мечтать.