Initial release: collaborative trip planner
Multi-user trip planning web app in a single Docker container. Mullvad-style token accounts, trip sharing via join codes, day-by-day calendar with typed entries (activity, hotel, travel, flight, rental car, immigration, note), multi-leg flight segments with bundled IATA airport dataset, Leaflet/OSM map with per-leg great-circle km (air vs ground), rough km-driven vs rental included-km comparison, cost splitting with settle-up suggestions, flip-clock departure countdown. Node 20 + Express + SQLite (WAL, additive migrations), vanilla JS SPA, 44 API tests.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
// Thin fetch wrapper for the Trip Plan REST API (see docs/API.md).
|
||||
// Same-origin, cookies included. Server errors ({error}) become thrown Errors
|
||||
// whose message is the server's human-readable text and `.status` the code.
|
||||
|
||||
async function request(method, path, body) {
|
||||
const opts = {
|
||||
method,
|
||||
credentials: 'same-origin',
|
||||
headers: {},
|
||||
};
|
||||
if (body !== undefined) {
|
||||
opts.headers['Content-Type'] = 'application/json';
|
||||
opts.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(path, opts);
|
||||
} catch (networkErr) {
|
||||
const err = new Error('Network error — is the server running?');
|
||||
err.cause = networkErr;
|
||||
err.status = 0;
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (res.status === 204) return null;
|
||||
|
||||
const text = await res.text();
|
||||
let data = null;
|
||||
if (text) {
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
data = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
const message = (data && data.error) || `Request failed (${res.status})`;
|
||||
const err = new Error(message);
|
||||
err.status = res.status;
|
||||
err.body = data;
|
||||
throw err;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const get = (p) => request('GET', p);
|
||||
const post = (p, b) => request('POST', p, b);
|
||||
const patch = (p, b) => request('PATCH', p, b);
|
||||
const del = (p) => request('DELETE', p);
|
||||
|
||||
export const api = {
|
||||
auth: {
|
||||
// Mullvad-style: create an account (server returns the one-time token).
|
||||
createAccount: () => post('/api/auth/account', {}),
|
||||
login: (token) => post('/api/auth/login', { token }),
|
||||
logout: () => post('/api/auth/logout'),
|
||||
me: () => get('/api/auth/me'),
|
||||
updateMe: (patchBody) => patch('/api/auth/me', patchBody),
|
||||
},
|
||||
trips: {
|
||||
list: () => get('/api/trips'),
|
||||
create: (payload) => post('/api/trips', payload),
|
||||
get: (id) => get(`/api/trips/${id}`),
|
||||
update: (id, patchBody) => patch(`/api/trips/${id}`, patchBody),
|
||||
remove: (id) => del(`/api/trips/${id}`),
|
||||
join: (code) => post('/api/trips/join', { code }),
|
||||
regenerateJoinCode: (id) => post(`/api/trips/${id}/join-code`, {}),
|
||||
removeMember: (id, userId) => del(`/api/trips/${id}/members/${userId}`),
|
||||
route: (id) => get(`/api/trips/${id}/route`),
|
||||
costs: (id) => get(`/api/trips/${id}/costs`),
|
||||
},
|
||||
entries: {
|
||||
create: (tripId, payload) => post(`/api/trips/${tripId}/entries`, payload),
|
||||
update: (id, patchBody) => patch(`/api/entries/${id}`, patchBody),
|
||||
remove: (id) => del(`/api/entries/${id}`),
|
||||
},
|
||||
geocode: (q) => get(`/api/geocode?q=${encodeURIComponent(q)}`),
|
||||
airports: (q) => get(`/api/airports?q=${encodeURIComponent(q)}`),
|
||||
};
|
||||
|
||||
export default api;
|
||||
Reference in New Issue
Block a user