Проблема: app.js вызывал UserProfileView.render при showView('user-profile'),
но view был не создан. ReferenceError.
Создал UserProfileView в profile.js:
- Читает /users/:id + /users/:id/stats
- Card с avatar-b.lg, name, city, ★ rating (если есть отзывы)
- bio
- Кнопки: 'Написать' (ensureChat → openChat) + 'Все отзывы' (UserReviewsView)
- Error/toast на любой сбой
API: добавил UserApi.get|reviews|stats namespace.
149 lines
6.0 KiB
JavaScript
149 lines
6.0 KiB
JavaScript
// Same-origin API wrapper — обычный скрипт, без ES modules (Safari iOS)
|
|
// Используем только credentials:'include' + кэш отключен
|
|
|
|
const DEFAULT_INIT = {
|
|
credentials: 'include',
|
|
cache: 'no-store',
|
|
redirect: 'follow',
|
|
};
|
|
|
|
async function api(path, options) {
|
|
options = options || {};
|
|
const json = options.json;
|
|
const extraHeaders = options.headers;
|
|
const rest = options;
|
|
delete rest.json;
|
|
delete rest.headers;
|
|
|
|
const headers = new Headers(extraHeaders || {});
|
|
let body = rest.body;
|
|
|
|
if (json !== undefined) {
|
|
headers.set('Content-Type', 'application/json');
|
|
body = JSON.stringify(json);
|
|
}
|
|
if (!headers.has('Accept')) {
|
|
headers.set('Accept', 'application/json');
|
|
}
|
|
|
|
const res = await fetch(path, {
|
|
...DEFAULT_INIT,
|
|
...rest,
|
|
headers,
|
|
body,
|
|
});
|
|
|
|
const ct = res.headers.get('content-type') || '';
|
|
const isJson = ct.indexOf('application/json') >= 0;
|
|
const data = isJson ? await res.json().catch(function () { return null; }) : await res.text();
|
|
|
|
if (!res.ok) {
|
|
const err = new Error((data && (data.message || data.error)) || ('HTTP ' + res.status));
|
|
err.status = res.status;
|
|
err.data = data;
|
|
throw err;
|
|
}
|
|
return data;
|
|
}
|
|
|
|
const apiGet = function (path, opts) { return api(path, Object.assign({}, opts, { method: 'GET' })); };
|
|
const apiPost = function (path, json, opts) { return api(path, Object.assign({}, opts, { method: 'POST', json: json })); };
|
|
const apiPut = function (path, json, opts) { return api(path, Object.assign({}, opts, { method: 'PUT', json: json })); };
|
|
const apiDelete = function (path, opts) { return api(path, Object.assign({}, opts, { method: 'DELETE' })); };
|
|
|
|
const Store = {
|
|
access: null,
|
|
refresh: null,
|
|
user: null,
|
|
_key: 'buhapp_session_v1',
|
|
save: function (tokens) {
|
|
if (!tokens) return;
|
|
this.access = tokens.Access || tokens.access;
|
|
this.refresh = tokens.Refresh || tokens.refresh;
|
|
try {
|
|
localStorage.setItem(this._key, JSON.stringify({
|
|
access: this.access,
|
|
refresh: this.refresh,
|
|
}));
|
|
} catch (e) { /* localStorage недоступен (Safari private mode) — игнорим */ }
|
|
},
|
|
load: function () {
|
|
try {
|
|
const raw = localStorage.getItem(this._key);
|
|
if (!raw) return null;
|
|
const data = JSON.parse(raw);
|
|
this.access = data.access || null;
|
|
this.refresh = data.refresh || null;
|
|
return this.access;
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
},
|
|
clear: function () {
|
|
this.access = null;
|
|
this.refresh = null;
|
|
this.user = null;
|
|
try { localStorage.removeItem(this._key); } catch (e) {}
|
|
},
|
|
};
|
|
|
|
// сразу восстановим сессию при загрузке скрипта (нужно для refresh страницы)
|
|
Store.load();
|
|
|
|
const Auth = {
|
|
loginEmail: function (email, password, remember) { return apiPost('/api/v1/auth/login', { email: email, password: password, remember: !!remember }); },
|
|
registerEmail: function (data) { return apiPost('/api/v1/auth/register', data); },
|
|
me: function () { return apiGet('/api/v1/me'); },
|
|
updateMe: function (p) { return apiPut('/api/v1/me', p); },
|
|
getPrefs: function () { return apiGet('/api/v1/me/prefs'); },
|
|
updatePrefs: function (p) { return apiPut('/api/v1/me/prefs', p); },
|
|
updateLocation: function (lat, lng, visible) { return apiPut('/api/v1/me/location', { lat: lat, lng: lng, visible: visible }); },
|
|
setVisibility: function (v) { return apiPut('/api/v1/me/visibility', { visible: v }); },
|
|
searchNearby: function (lat, lng, radius) { return apiGet('/api/v1/search/nearby?lat=' + lat + '&lng=' + lng + '&radius=' + radius); },
|
|
getUser: function (id) { return apiGet('/api/v1/users/' + id); },
|
|
};
|
|
|
|
// === API namespaces ===
|
|
const UserApi = {
|
|
get: function (userId) { return apiGet('/api/v1/users/' + userId); },
|
|
reviews: function (userId) { return apiGet('/api/v1/users/' + userId + '/reviews'); },
|
|
stats: function (userId) { return apiGet('/api/v1/users/' + userId + '/stats'); },
|
|
};
|
|
|
|
const ChatApi = {
|
|
ensureChat: function (otherId) { return apiPost('/api/v1/chats', { other_id: otherId }); },
|
|
listChats: function () { return apiGet('/api/v1/chats'); },
|
|
listMessages: function (chatId) { return apiGet('/api/v1/chats/' + chatId + '/messages'); },
|
|
sendMessage: function (chatId, body, photoUrl) { return apiPost('/api/v1/chats/' + chatId + '/messages', { body: body, photo_url: photoUrl }); },
|
|
editMessage: function (msgId, body) { return apiPut('/api/v1/messages/' + msgId, { body: body }); },
|
|
deleteMessage: function (msgId) { return apiDelete('/api/v1/messages/' + msgId); },
|
|
markRead: function (chatId) { return apiPut('/api/v1/chats/' + chatId + '/read', {}); },
|
|
block: function (userId) { return apiPost('/api/v1/blocks', { user_id: userId }); },
|
|
report: function (type, id, reason) { return apiPost('/api/v1/reports', { target_type: type, target_id: id, reason: reason }); },
|
|
};
|
|
|
|
const ReviewApi = {
|
|
create: function (chatId, rating, body, anonymous) { return apiPost('/api/v1/reviews', { chat_id: chatId, rating: rating, body: body, anonymous: anonymous }); },
|
|
listForUser: function (userId) { return apiGet('/api/v1/users/' + userId + '/reviews'); },
|
|
stats: function (userId) { return apiGet('/api/v1/users/' + userId + '/stats'); },
|
|
};
|
|
|
|
const ConsentApi = {
|
|
status: function () { return apiGet('/api/v1/auth/consents').catch(function () { return { all_accepted: false, accepted: {} }; }); },
|
|
accept: function (payload) { return apiPost('/api/v1/auth/consents', payload); },
|
|
};
|
|
|
|
const PlacesApi = {
|
|
nearby: function (lat, lng, radius, limit) {
|
|
limit = limit || 30;
|
|
return apiGet('/api/v1/places/nearby?lat=' + lat + '&lng=' + lng + '&radius=' + (radius || 5) + '&limit=' + limit)
|
|
.catch(function (e) { return { places: [], count: 0, error: e.message }; });
|
|
},
|
|
};
|
|
|
|
window.API = {
|
|
api: api, apiGet: apiGet, apiPost: apiPost, apiPut: apiPut, apiDelete: apiDelete,
|
|
Auth: Auth, ChatApi: ChatApi, ReviewApi: ReviewApi, ConsentApi: ConsentApi, PlacesApi: PlacesApi,
|
|
Store: Store,
|
|
};
|