ДИЗАЙН-СИСТЕМА в 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 кнопок.
160 lines
6.8 KiB
JavaScript
160 lines
6.8 KiB
JavaScript
// 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; |