buhapp-telegram/public/js/api.js
root 7a27b2cf3f Map: 2GIS tiles + places (бары/рестораны)
- L.tileLayer 2GIS
- PlacesApi.nearby() в client
- Кнопка 'Показать/Скрыть бары'
- Маркеры баров с эмодзи (🍺🍽️🎉)
- Список под картой (карточки)
- Invite to place: отправка предложения в чат
- 2GIS deeplink в сообщении
2026-08-20 21:12:05 +00:00

186 lines
5.6 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.

// API клиент
const API_BASE = window.API_BASE || 'https://api.buhapp.mygoodservice.ru';
const Store = {
access: null,
refresh: null,
user: null,
save(tokens) {
if (!tokens) return;
// бэкенд отдаёт {Access, Refresh} — поддержим оба варианта
this.access = tokens.Access || tokens.access;
this.refresh = tokens.Refresh || tokens.refresh;
try {
localStorage.setItem('buhapp', JSON.stringify({
access: this.access,
refresh: this.refresh,
user: this.user,
}));
} catch {}
},
load() {
try {
const s = JSON.parse(localStorage.getItem('buhapp') || 'null');
if (s) {
this.access = s.access || s.Access;
this.refresh = s.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 ${Store.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'); },
};
const ConsentApi = {
async status() {
try {
return await api('/api/v1/auth/consents');
} catch (e) {
return { all_accepted: false, accepted: {} };
}
},
async accept(payload) {
return api('/api/v1/auth/consents', { method: 'POST', body: payload });
},
};
const PlacesApi = {
async nearby(lat, lng, radius, limit = 30) {
try {
return await api(`/api/v1/places/nearby?lat=${lat}&lng=${lng}&radius=${radius || 5}&limit=${limit}`);
} catch (e) {
return { places: [], count: 0, error: e.message };
}
},
};
window.API = { api, Auth, ChatApi, ReviewApi, ConsentApi, PlacesApi, Store };