diff --git a/public/js/api.js b/public/js/api.js
index 36cdbc4..e40a6d1 100644
--- a/public/js/api.js
+++ b/public/js/api.js
@@ -43,23 +43,41 @@ const Store = {
async function api(path, opts = {}) {
opts.headers = opts.headers || {};
// НЕ шлём Authorization — используем HttpOnly cookie (без CORS preflight)
- // Middleware читает cookie buhapp_at
if (opts.body && typeof opts.body === 'object' && !(opts.body instanceof FormData)) {
opts.headers['Content-Type'] = 'application/json';
opts.body = JSON.stringify(opts.body);
}
- let r = await fetch(API_BASE + path, { ...opts, credentials: 'include' });
- const text = await r.text();
- let data = null;
- try { data = text ? JSON.parse(text) : null; } catch { data = text; }
- if (!r.ok) {
- const msg = (data && data.error) || `HTTP ${r.status}`;
- const err = new Error(msg);
- err.status = r.status;
- err.data = data;
- throw err;
+ opts.headers['Accept'] = opts.headers['Accept'] || 'application/json';
+
+ // timeout 20s — Safari иногда зависает на ответе после keep-alive
+ const controller = new AbortController();
+ const tid = setTimeout(() => controller.abort(), 20000);
+ try {
+ let r = await fetch(API_BASE + path, {
+ ...opts,
+ credentials: 'include',
+ cache: 'no-store',
+ mode: 'cors',
+ redirect: 'follow',
+ signal: controller.signal,
+ });
+ const text = await r.text();
+ let data = null;
+ try { data = text ? JSON.parse(text) : null; } catch { data = text; }
+ if (!r.ok) {
+ const msg = (data && data.error) || `HTTP ${r.status}`;
+ const err = new Error(msg);
+ err.status = r.status;
+ err.data = data;
+ throw err;
+ }
+ return data;
+ } catch (e) {
+ if (e.name === 'AbortError') throw new Error('Сервер не отвечает (таймаут 20с)');
+ throw e;
+ } finally {
+ clearTimeout(tid);
}
- return data;
}
// === Auth ===
diff --git a/public/js/app.js b/public/js/app.js
index dab263b..966ea75 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -50,7 +50,6 @@ const App = {
this.showError('Сессия истекла: ' + e.message);
}
} else {
- // Вне Telegram — показать email login (но сначала согласия)
this.showWelcome();
}
@@ -214,8 +213,12 @@ const App = {
-
-
Введите email и пароль
+
+ Введите email и пароль (≥8 символов)
+
+
+
👥 Недавно зарегистрировались:
+
Загрузка...
Регистрируясь, вы подтверждаете совершеннолетие (18+) и принимаете
@@ -231,21 +234,54 @@ const App = {
const pass = document.getElementById('el-pass').value;
const ok = email.length > 0 && pass.length >= 8;
document.getElementById('el-login').disabled = !ok;
- document.getElementById('el-register').disabled = !ok;
const hint = document.getElementById('el-hint');
if (pass.length > 0 && pass.length < 8) {
hint.textContent = 'Пароль слишком короткий (' + pass.length + '/8)';
hint.style.color = 'var(--danger)';
} else if (ok) {
- hint.textContent = '✓ Готово';
+ hint.textContent = '✓ Готово — нажми Войти';
hint.style.color = 'var(--success)';
+ } else if (pass.length > 0) {
+ hint.textContent = 'Введите email';
+ hint.style.color = 'var(--text-dim)';
} else {
- hint.textContent = 'Введите email и пароль';
+ hint.textContent = 'Введите email и пароль (≥8 символов)';
hint.style.color = 'var(--text-dim)';
}
};
document.getElementById('el-email').addEventListener('input', update);
document.getElementById('el-pass').addEventListener('input', update);
+
+ // подгрузим недавно зарегистрированных
+ this.loadRecentUsers();
+ },
+
+ async loadRecentUsers() {
+ try {
+ const r = await api('/api/v1/users/recent?limit=8');
+ const box = document.getElementById('recent-users-list');
+ if (!box) return;
+ if (!r.users || !r.users.length) {
+ box.innerHTML = '
Пока никого нет. Будь первым!
';
+ box.className = '';
+ return;
+ }
+ box.className = '';
+ box.innerHTML = r.users.map((u) => `
+
+
${escapeHtml((u.name || '?')[0].toUpperCase())}
+
+
${escapeHtml(u.name)}
+
${u.age ? u.age + ' лет' : ''}${u.city ? ' • ' + escapeHtml(u.city) : ''}
+
+
${u.registered_ago || ''}
+
+ `).join('');
+ } catch (e) {
+ const box = document.getElementById('recent-users-list');
+ if (box) { box.innerHTML = ''; box.className = ''; }
+ console.warn('loadRecentUsers:', e);
+ }
},
// Сначала consent overlay, потом реальная регистрация
@@ -266,9 +302,10 @@ const App = {
const pass = document.getElementById('el-pass').value;
try {
await API.Auth.loginEmail(email, pass);
+ // Safari: дать применить Set-Cookie перед следующим fetch
+ await new Promise((r) => setTimeout(r, 150));
await API.Auth.me();
- this.onLogin(); // показать таббар
- // для email-логина согласия можно проверить, но юзер их уже дал
+ this.onLogin();
this.maybeOnboard();
this.showView('map');
} catch (e) {