- index.html with bottom tabbar (Map/Chats/Profile/Settings) - Telegram WebApp SDK auth via /api/v1/auth/telegram - Consent gate on first login (4 checkboxes) - Map view with Leaflet (OSM) + nearby users + radius selector - Chat list + single chat with WebSocket realtime - Profile editing (drinks, activities, purposes) - Review screen with stars + anonymous toggle - User reviews list with stats - User profile (public) with rating, write/block/report - Legal pages (TERMS / PRIVACY / DISCLAIMER)
151 lines
4.7 KiB
JavaScript
151 lines
4.7 KiB
JavaScript
// API клиент
|
|
const API_BASE = window.API_BASE || 'https://api.buhapp.mygoodservice.ru';
|
|
|
|
const Store = {
|
|
access: null,
|
|
refresh: null,
|
|
user: null,
|
|
|
|
save(tokens) {
|
|
this.access = tokens.access;
|
|
this.refresh = tokens.refresh;
|
|
try { localStorage.setItem('buhapp', JSON.stringify({ access, refresh, user: this.user })); } catch {}
|
|
},
|
|
|
|
load() {
|
|
try {
|
|
const s = JSON.parse(localStorage.getItem('buhapp') || 'null');
|
|
if (s) { this.access = s.access; this.refresh = s.refresh; this.user = s.user; }
|
|
} catch {}
|
|
return this.access;
|
|
},
|
|
|
|
clear() {
|
|
this.access = null;
|
|
this.refresh = null;
|
|
this.user = null;
|
|
try { localStorage.removeItem('buhapp'); } catch {}
|
|
},
|
|
};
|
|
|
|
async function api(path, opts = {}) {
|
|
opts.headers = opts.headers || {};
|
|
if (Store.access && !opts.headers.Authorization) {
|
|
opts.headers.Authorization = `Bearer ${Store.access}`;
|
|
}
|
|
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);
|
|
if (r.status === 401 && Store.refresh) {
|
|
// refresh
|
|
const r2 = await fetch(API_BASE + '/api/v1/auth/refresh', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ refresh: Store.refresh }),
|
|
});
|
|
if (r2.ok) {
|
|
const tokens = await r2.json();
|
|
Store.save(tokens);
|
|
opts.headers.Authorization = `Bearer ${tokens.access}`;
|
|
r = await fetch(API_BASE + path, opts);
|
|
} else {
|
|
Store.clear();
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
|
|
// === Auth ===
|
|
|
|
const Auth = {
|
|
async loginWithTelegram(initData) {
|
|
const r = await api('/api/v1/auth/telegram', {
|
|
method: 'POST',
|
|
body: { init_data: initData },
|
|
});
|
|
Store.save(r.tokens);
|
|
Store.user = r.user;
|
|
return r;
|
|
},
|
|
|
|
async registerEmail(data) {
|
|
const r = await api('/api/v1/auth/register', { method: 'POST', body: data });
|
|
Store.save(r.tokens);
|
|
Store.user = r.user;
|
|
return r;
|
|
},
|
|
|
|
async loginEmail(email, password) {
|
|
const r = await api('/api/v1/auth/login', { method: 'POST', body: { email, password } });
|
|
Store.save(r.tokens);
|
|
Store.user = r.user;
|
|
return r;
|
|
},
|
|
|
|
async me() {
|
|
const r = await api('/api/v1/me');
|
|
Store.user = r.user;
|
|
return r;
|
|
},
|
|
|
|
async updateMe(p) {
|
|
return api('/api/v1/me', { method: 'PUT', body: p });
|
|
},
|
|
|
|
async getPrefs() { return api('/api/v1/me/prefs'); },
|
|
async updatePrefs(p) { return api('/api/v1/me/prefs', { method: 'PUT', body: p }); },
|
|
async updateLocation(lat, lng, visible) {
|
|
return api('/api/v1/me/location', { method: 'PUT', body: { lat, lng, visible } });
|
|
},
|
|
async setVisibility(v) { return api('/api/v1/me/visibility', { method: 'PUT', body: { visible: v } }); },
|
|
async searchNearby(lat, lng, radiusKm) {
|
|
return api('/api/v1/search/nearby?lat=' + lat + '&lng=' + lng + '&radius=' + radiusKm);
|
|
},
|
|
async getUser(id) { return api('/api/v1/users/' + id); },
|
|
};
|
|
|
|
// === Chat ===
|
|
|
|
const ChatApi = {
|
|
async ensureChat(otherId) { return api('/api/v1/chats', { method: 'POST', body: { other_id: otherId } }); },
|
|
async listChats() { return api('/api/v1/chats'); },
|
|
async listMessages(chatId) { return api('/api/v1/chats/' + chatId + '/messages'); },
|
|
async sendMessage(chatId, body, photoUrl) {
|
|
return api('/api/v1/chats/' + chatId + '/messages', { method: 'POST', body: { body, photo_url: photoUrl } });
|
|
},
|
|
async editMessage(msgId, body) {
|
|
return api('/api/v1/messages/' + msgId, { method: 'PUT', body: { body } });
|
|
},
|
|
async deleteMessage(msgId) {
|
|
return api('/api/v1/messages/' + msgId, { method: 'DELETE' });
|
|
},
|
|
async markRead(chatId) {
|
|
return api('/api/v1/chats/' + chatId + '/read', { method: 'PUT', body: {} });
|
|
},
|
|
async block(userId) { return api('/api/v1/blocks', { method: 'POST', body: { user_id: userId } }); },
|
|
async report(type, id, reason) { return api('/api/v1/reports', { method: 'POST', body: { target_type: type, target_id: id, reason } }); },
|
|
};
|
|
|
|
// === Reviews ===
|
|
|
|
const ReviewApi = {
|
|
async create(chatId, rating, body, anonymous) {
|
|
return api('/api/v1/reviews', { method: 'POST', body: { chat_id: chatId, rating, body, anonymous } });
|
|
},
|
|
async listForUser(userId) { return api('/api/v1/users/' + userId + '/reviews'); },
|
|
async stats(userId) { return api('/api/v1/users/' + userId + '/stats'); },
|
|
};
|
|
|
|
window.API = { api, Auth, ChatApi, ReviewApi, Store }; |