buhapp-telegram/public/js/api.js
root 21d281a1c6 Telegram WebApp: Mini App with map, chats, profile, reviews
- 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)
2026-08-20 17:43:16 +00:00

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 };