buhapp-telegram/public/js/views/review.js
root c5f8ac67ad UX overhaul: единый стиль BuhApp на всех экранах
ДИЗАЙН-СИСТЕМА в app.css:
- --primary #FF6B35, --success #3dd68c, --danger #ff5c5c
- --bg #0f0f12, --bg-elev #1a1a22, --bg-input #121218
- Компоненты: .card-b, .row-b, .field-b, .btn-b, .switch-b, .chip, .avatar-b, .bubble-b, .composer-b, .toast-b, .stars-b, .pulse-b, .empty-b

ПЕРЕПИСАНЫ на единый стиль:

Settings:
- Карточка с аватаром пользователя
- Card-row для юр.документов (📄 / 🔒 / ⚠️)
- 'Выйти' — danger btn-b с state machine (idle→load→ok)

Profile:
- Карточка аватара + кнопка 'Загрузить фото' через multipart /api/v1/me/photo
- Поля: field-b (ctrl + hint)
- Чипы в chip-list, новый стиль с .on
- Счётчик bio символов
- 'Сохранить' со state machine + toast

Review (создание):
- Звёзды в stars-b с center hint
- textarea с counter
- switch-b 'Анонимно' (вместо checkbox)
- Submit со state machine + toast 'Спасибо за отзыв!'

UserReviews:
- Средний рейтинг + звёзды
- Список карточек с отзывами

Chats:
- Список с avatar-b + last message + время + badge непрочитанных
- Пустое состояние с эмодзи
- formatChatTime() — 'сейчас' / '5м' / '3ч' / '2д'

Chat (один):
- Header с аватаром и кнопками 👤 ⭐
- composer-b: 📎 + input + ➤ (sticky bottom)
- bubble-b .in / .out с .ticks.read
- Send со state + toast
- sendPhoto() для фоток в чате

App.js _toast(msg, kind):
- Единый toast-b с классами ok / err

Всё в одном стиле. CSS variables, padding 16, radius 14, no disabled кнопок.
2026-08-21 11:42:16 +00:00

160 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Review screens — create + user-reviews list
const ReviewView = {
rating: 5,
chatId: null,
otherId: null,
otherName: null,
async render(root, params) {
this.chatId = params.chatId;
this.otherId = params.otherId;
this.otherName = params.otherName;
this.rating = 5;
root.innerHTML = `
<div class="view">
<h1 style="margin:0 0 6px;font-size:22px;">⭐ Отзыв</h1>
<div style="color:var(--text-dim);font-size:13px;margin-bottom:14px;">О ${escapeHtml(this.otherName || 'пользователе')}</div>
<div class="card-b">
<div style="font-size:12px;font-weight:600;color:var(--text-dim);">Ваша оценка</div>
<div class="stars-b" id="stars">
${[1,2,3,4,5].map(n => `<span class="star on" data-n="${n}">★</span>`).join('')}
</div>
<div class="hint mute" id="r-rating-hint" style="text-align:center;color:var(--success);font-size:13px;margin-bottom:14px;">5 из 5</div>
<label class="field-b" style="margin-top:6px;">
<span style="font-size:12px;font-weight:600;color:var(--text-dim);">Комментарий (необязательно)</span>
<textarea class="ctrl" id="r-body" rows="4" placeholder="Как прошла встреча?" maxlength="2000" style="resize:none;"></textarea>
<div class="hint mute" id="r-counter" style="text-align:right;font-size:11px;">0 / 2000</div>
</label>
<label class="row-b" style="margin-top:4px;">
<div class="title" style="font-size:14px;font-weight:500;">Анонимный отзыв</div>
<input type="checkbox" id="r-anon" style="display:none;">
<div class="switch-b" id="r-anon-sw"></div>
</label>
<button class="btn-b" id="r-submit" style="margin-top:16px;">Отправить</button>
</div>
</div>
`;
// звёзды
document.querySelectorAll('#stars .star').forEach(s => {
s.onclick = () => {
this.rating = parseInt(s.dataset.n, 10);
document.querySelectorAll('#stars .star').forEach((x, i) => {
x.classList.toggle('on', i < this.rating);
});
const h = document.getElementById('r-rating-hint');
h.textContent = this.rating + ' из 5';
h.style.color = 'var(--success)';
TG_APP.haptic('selection');
};
});
// чекбокс как switch
document.getElementById('r-anon-sw').onclick = () => {
const sw = document.getElementById('r-anon-sw');
const cb = document.getElementById('r-anon');
const newVal = !cb.checked;
cb.checked = newVal;
sw.classList.toggle('on', newVal);
};
// счётчик textarea
document.getElementById('r-body').addEventListener('input', (e) => {
const c = document.getElementById('r-counter');
const n = e.target.value.length;
c.textContent = n + ' / 2000';
c.className = 'hint ' + (n > 1800 ? 'err' : 'mute');
});
document.getElementById('r-submit').onclick = () => this.submit();
},
async submit() {
const btn = document.getElementById('r-submit');
const orig = btn.textContent;
btn.className = 'btn-b load';
btn.textContent = '⏳ Отправляем…';
try {
await API.ReviewApi.create(
this.chatId,
this.rating,
document.getElementById('r-body').value.trim(),
document.getElementById('r-anon').checked
);
TG_APP.haptic('notification');
btn.className = 'btn-b ok';
btn.textContent = '✓ Готово!';
App._toast('Спасибо за отзыв!', 'ok');
setTimeout(() => {
btn.className = 'btn-b';
btn.textContent = orig;
App.showView('chats');
}, 1200);
} catch (e) {
btn.className = 'btn-b err';
btn.textContent = '⚠️ ' + (e.message || 'Ошибка');
App._toast('Не удалось отправить', 'err');
setTimeout(() => {
btn.className = 'btn-b';
btn.textContent = orig;
}, 2000);
}
},
};
const UserReviewsView = {
async render(root, params) {
const userId = params.userId;
const userName = params.userName || 'Пользователь';
root.innerHTML = `
<div class="view">
<h1 style="margin:0 0 14px;font-size:22px;">⭐ Отзывы о ${escapeHtml(userName)}</h1>
<div id="ur-header" class="empty-b">⏳ Загрузка…</div>
<div id="ur-list" class="row-list" style="margin-top:14px;"></div>
</div>
`;
try {
const [stats, list] = await Promise.all([
API.ReviewApi.stats(userId),
API.ReviewApi.listForUser(userId),
]);
const avg = stats.rating_avg ? Number(stats.rating_avg).toFixed(1) : '—';
const header = document.getElementById('ur-header');
header.className = 'card-b';
header.innerHTML = `
<div style="display:flex;align-items:center;gap:14px;">
<div style="font-size:36px;font-weight:700;color:var(--warn);">${avg}</div>
<div style="flex:1;">
<div style="font-size:18px;color:var(--warn);">${[1,2,3,4,5].map(n => `<span style="color:${n<=Math.round(stats.rating_avg||0)?'var(--warn)':'var(--border-strong)'}">★</span>`).join('')}</div>
<div style="font-size:13px;color:var(--text-dim);margin-top:2px;">${stats.rating_count || 0} отзывов</div>
</div>
</div>
`;
const box = document.getElementById('ur-list');
if (!list.reviews || list.reviews.length === 0) {
box.innerHTML = '<div class="empty-b">Пока нет отзывов</div>';
} else {
list.reviews.forEach((r) => {
const card = document.createElement('div');
card.className = 'card-b';
card.style.padding = '14px';
card.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;">
<div style="font-size:16px;color:var(--warn);">${[1,2,3,4,5].map(n=>`<span style="color:${n<=r.rating?'var(--warn)':'var(--border-strong)'}">★</span>`).join('')}</div>
<div style="font-size:11px;color:var(--text-mute);">${formatDate(r.created_at)}</div>
</div>
<div style="font-size:12px;color:var(--text-dim);margin:4px 0 6px;">${r.anonymous ? 'Анонимно' : 'ID: ' + (r.reviewer_id || '').slice(0,8)}</div>
${r.body ? '<div style="font-size:14px;">' + escapeHtml(r.body) + '</div>' : ''}
`;
box.appendChild(card);
});
}
} catch (e) {
document.getElementById('ur-header').innerHTML = '<div class="empty-b">⚠️ Ошибка загрузки</div>';
App._toast('Не удалось загрузить отзывы', 'err');
}
},
};
window.ReviewView = ReviewView;
window.UserReviewsView = UserReviewsView;