// api-client.jsx — Client JS qui parle au backend PHP const API_BASE = (() => { // En dev local : pas d'API, mode démo (détecté par l'absence de TLD) var h = location.hostname; var isDevHost = !h.includes('.') || h === '127.0.0.1' || h.startsWith('192.168.') || h.startsWith('10.'); if (isDevHost) return null; return location.pathname.includes('/app/') ? '/app/api' : '/api'; })(); // === Token storage === const TOKEN_KEY = 'trusta_token'; const USER_KEY = 'trusta_user'; const Auth = { getToken: () => { try { return localStorage.getItem(TOKEN_KEY); } catch (e) { return null; } }, setToken: (t) => { try { localStorage.setItem(TOKEN_KEY, t); } catch (e) {} }, clearToken: () => { try { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(USER_KEY); } catch (e) {} }, getUser: () => { try { return JSON.parse(localStorage.getItem(USER_KEY) || 'null'); } catch (e) { return null; } }, setUser: (u) => { try { localStorage.setItem(USER_KEY, JSON.stringify(u)); } catch (e) {} }, isLoggedIn: () => !!localStorage.getItem(TOKEN_KEY), }; window.TrustaAuth = Auth; // === Fetch helper avec gestion auth + erreurs === async function apiFetch(path, options = {}) { if (!API_BASE) { throw new Error('API not available in dev mode'); } const url = API_BASE + path; const headers = options.headers || {}; const token = Auth.getToken(); if (token) headers['Authorization'] = 'Bearer ' + token; // Envoie la langue de l'interface au backend pour les contenus auto-traduits (annonces, etc.) try { var lang = localStorage.getItem('trusta_lang') || 'ka'; if (lang === 'ka' || lang === 'fr') headers['X-Lang'] = lang; } catch (e) {} // Auto JSON content-type pour body objet if (options.body && !(options.body instanceof FormData) && typeof options.body === 'object') { headers['Content-Type'] = 'application/json'; options.body = JSON.stringify(options.body); } let res; try { res = await fetch(url, { ...options, headers }); } catch (e) { throw { code: 'network_error', message: 'Connexion impossible' }; } let data; try { data = await res.json(); } catch (e) { data = {}; } if (!res.ok) { if (res.status === 401) { Auth.clearToken(); } throw { code: data.error || 'http_error', message: data.message || `HTTP ${res.status}`, status: res.status, }; } return data; } // === API methods === window.TrustaAPI = { // Auth requestOtp: (phone, locale) => apiFetch('/auth/request-otp.php', { method: 'POST', body: { phone, locale } }), verifyOtp: (phone, code) => apiFetch('/auth/verify-otp.php', { method: 'POST', body: { phone, code } }) .then(r => { if (r.token) { Auth.setToken(r.token); if (r.user) Auth.setUser(r.user); } return r; }), logout: () => apiFetch('/auth/logout.php', { method: 'POST' }) .finally(() => Auth.clearToken()), // Profil getMe: () => apiFetch('/me.php').then(r => { if (r.user) Auth.setUser(r.user); return r.user; }), updateMe: (updates) => apiFetch('/me.php', { method: 'POST', body: updates }) .then(r => { if (r.user) Auth.setUser(r.user); return r.user; }), uploadProfilePhoto: (file) => { const fd = new FormData(); fd.append('photo', file); return apiFetch('/me-photo.php', { method: 'POST', body: fd }); }, // Listings listListings: (filters = {}) => { const qs = new URLSearchParams(Object.entries(filters).filter(([_, v]) => v !== '' && v != null)).toString(); return apiFetch('/listings/index.php' + (qs ? '?' + qs : '')) .then(r => r.listings); }, getListing: (id) => apiFetch(`/listings/show.php?id=${id}`).then(r => r.listing), createListing: (data) => apiFetch('/listings/index.php', { method: 'POST', body: data }).then(r => r.listing), updateListing: (id, data) => apiFetch(`/listings/show.php?id=${id}`, { method: 'PUT', body: data }).then(r => r.listing), deleteListing: (id) => apiFetch(`/listings/show.php?id=${id}`, { method: 'DELETE' }), uploadListingPhoto: (file, listingId = null) => { const fd = new FormData(); fd.append('photo', file); const path = '/listings/photos.php' + (listingId ? `?listing_id=${listingId}` : ''); return apiFetch(path, { method: 'POST', body: fd }); }, // === Chat === listConversations: () => apiFetch('/chat/conversations.php').then(r => r.conversations), openConversation: (peerId, listingId = null) => apiFetch('/chat/conversations.php', { method: 'POST', body: { peer_id: peerId, listing_id: listingId }, }).then(r => r.conversation), listMessages: (conversationId, sinceId = 0) => { const qs = `?conversation_id=${conversationId}` + (sinceId ? `&since_id=${sinceId}` : ''); return apiFetch('/chat/messages.php' + qs).then(r => r.messages); }, sendMessage: (conversationId, body, photoUrl = null) => apiFetch('/chat/messages.php', { method: 'POST', body: { conversation_id: conversationId, body, photo_url: photoUrl }, }).then(r => r.message), // === Favoris === listFavorites: (kind) => apiFetch(`/favorites/index.php?kind=${kind}`).then(r => r.favorites), fetchFavoriteIds: () => apiFetch('/favorites/index.php?ids=1').then(r => r.ids), toggleFavorite: (kind, targetId) => apiFetch('/favorites/index.php', { method: 'POST', body: { kind, target_id: targetId }, }).then(r => r.favorited), // === Notifications === listNotifications: () => apiFetch('/notifications/index.php').then(r => ({ items: r.notifications, unread: r.unread })), unreadCount: () => apiFetch('/notifications/index.php?count=1').then(r => r.unread), markNotificationRead: (id) => apiFetch('/notifications/index.php', { method: 'POST', body: { id } }), markAllNotificationsRead: () => apiFetch('/notifications/index.php', { method: 'POST', body: { all: true } }), }; // === Store global notifications (badge unread synchronisé) === (function () { let unread = 0; let pollId = null; const listeners = new Set(); function notify() { listeners.forEach(fn => { try { fn(unread); } catch (e) {} }); } window.TrustaNotifStore = { getUnread() { return unread; }, subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); }, async refresh() { if (!window.API_AVAILABLE || !window.TrustaAuth || !window.TrustaAuth.isLoggedIn()) return; try { const n = await window.TrustaAPI.unreadCount(); unread = Number(n) || 0; notify(); } catch (e) { /* ignore */ } }, startPolling(intervalMs = 30000) { if (pollId) return; this.refresh(); pollId = setInterval(() => this.refresh(), intervalMs); }, stopPolling() { if (pollId) { clearInterval(pollId); pollId = null; } }, }; })(); // === Store global favoris (cache + listeners pour re-render multi-composants) === (function () { const state = { listing: new Set(), profile: new Set() }; const listeners = new Set(); function notify() { listeners.forEach(fn => { try { fn(); } catch (e) {} }); } window.TrustaFavStore = { isFav(kind, id) { return state[kind] && state[kind].has(Number(id)); }, subscribe(fn) { listeners.add(fn); return () => listeners.delete(fn); }, async refresh() { if (!window.API_AVAILABLE || !window.TrustaAuth || !window.TrustaAuth.isLoggedIn()) return; try { const ids = await window.TrustaAPI.fetchFavoriteIds(); state.listing = new Set((ids.listing || []).map(Number)); state.profile = new Set((ids.profile || []).map(Number)); notify(); } catch (e) { /* ignore */ } }, async toggle(kind, id) { const numId = Number(id); // Optimistic const had = state[kind].has(numId); if (had) state[kind].delete(numId); else state[kind].add(numId); notify(); try { const favorited = await window.TrustaAPI.toggleFavorite(kind, numId); // Reconcile au cas où le serveur a une autre vérité if (favorited) state[kind].add(numId); else state[kind].delete(numId); notify(); return favorited; } catch (e) { // Rollback if (had) state[kind].add(numId); else state[kind].delete(numId); notify(); throw e; } }, }; })(); window.API_AVAILABLE = !!API_BASE;