- AppReport entity + migration (type, message, fingerprint SHA-256, created_at) - POST /api/report avec rate-limit 5/heure par fingerprint IP - ReportModal : formulaire type radio (bug/idée) + textarea 1 000 chars max - ChangelogModal : fonctionnalités en prod, roadmap, 5 limitations honnêtes - Deux boutons flottants bas-gauche (? info + ✉ signalement) - Légende remontée pour éviter le chevauchement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
62 lines
1.9 KiB
JavaScript
62 lines
1.9 KiB
JavaScript
const BASE_URL = import.meta.env.VITE_API_URL ?? '/api';
|
|
|
|
async function get(path, params = {}) {
|
|
const url = new URL(BASE_URL + path, window.location.origin);
|
|
Object.entries(params).forEach(([k, v]) => v !== undefined && url.searchParams.set(k, v));
|
|
|
|
const res = await fetch(url.toString(), {
|
|
headers: { Accept: 'application/json' },
|
|
});
|
|
|
|
if (!res.ok) throw new Error(`API error ${res.status}: ${path}`);
|
|
return res.json();
|
|
}
|
|
|
|
async function post(path, body) {
|
|
const res = await fetch(BASE_URL + path, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
if (!res.ok) throw new Error(`API error ${res.status}: ${path}`);
|
|
return res.json();
|
|
}
|
|
|
|
export const api = {
|
|
spots: {
|
|
overview: () => get('/spots/overview'), // tous les spots + score en 1 requête cachée
|
|
get: (id) => get(`/spots/${id}`),
|
|
score: (id, at) => get(`/spots/${id}/score`, { at }),
|
|
},
|
|
observations: {
|
|
list: (bbox, date, source) => get('/observations', { bbox, date, source }),
|
|
get: (id) => get(`/observations/${id}`),
|
|
},
|
|
forecasts: {
|
|
list: (bbox, horizon) => get('/forecasts', { bbox, horizon }),
|
|
get: (id) => get(`/forecasts/${id}`),
|
|
},
|
|
feedback: {
|
|
create: (data) => post('/feedback', data),
|
|
},
|
|
status: {
|
|
get: () => get('/status'),
|
|
},
|
|
report: {
|
|
create: (data) => post('/report', data),
|
|
},
|
|
push: {
|
|
vapidKey: () => get('/push/vapid-public-key'),
|
|
subscribe: (data) => post('/push/subscribe', data),
|
|
unsubscribe: async (endpoint) => {
|
|
const res = await fetch(BASE_URL + '/push/subscribe', {
|
|
method: 'DELETE',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ endpoint }),
|
|
});
|
|
if (!res.ok && res.status !== 204) throw new Error(`API error ${res.status}`);
|
|
},
|
|
},
|
|
};
|