feat: decompose frontend into native ES modules (zero bundlers, zero npm)
This commit is contained in:
1 parent
869bf23948
commit
21d94acf4e
9 files changed
+4307
-3893
No files matched your search
@@ -0,0 +1,184 @@
|
||||
// =======================================================================
|
||||
// Notes Application - API Client Module (Native ES Module)
|
||||
// =======================================================================
|
||||
|
||||
// Persistent session: auto-inject Authorization header if token exists in localStorage
|
||||
const originalFetch = window.fetch;
|
||||
window.fetch = function(url, options = {}) {
|
||||
const token = localStorage.getItem('notes_token');
|
||||
if (token && typeof url === 'string' && url.startsWith('/api/')) {
|
||||
options.headers = options.headers || {};
|
||||
if (options.headers instanceof Headers) {
|
||||
if (!options.headers.has('Authorization')) {
|
||||
options.headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
} else if (Array.isArray(options.headers)) {
|
||||
if (!options.headers.some(([k]) => k.toLowerCase() === 'authorization')) {
|
||||
options.headers.push(['Authorization', `Bearer ${token}`]);
|
||||
}
|
||||
} else {
|
||||
if (!options.headers['Authorization']) {
|
||||
options.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return originalFetch.call(this, url, options);
|
||||
};
|
||||
|
||||
export const api = {
|
||||
// Auth endpoints
|
||||
auth: {
|
||||
async check() {
|
||||
const resp = await fetch('/api/me');
|
||||
if (resp.ok) {
|
||||
return await resp.json();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
async login(username, password) {
|
||||
return await fetch('/api/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
},
|
||||
async logout() {
|
||||
return await fetch('/api/logout', { method: 'POST' });
|
||||
},
|
||||
async changePassword(currentPassword, newPassword) {
|
||||
return await fetch('/api/change-password', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
old_password: currentPassword,
|
||||
new_password: newPassword
|
||||
})
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Cards & Vocabulary
|
||||
cards: {
|
||||
async getAll(params = {}) {
|
||||
const qs = new URLSearchParams(params).toString();
|
||||
const url = qs ? `/api/cards?${qs}` : '/api/cards';
|
||||
const resp = await fetch(url);
|
||||
return resp.ok ? await resp.json() : [];
|
||||
},
|
||||
async lookup(term) {
|
||||
const resp = await fetch(`/api/lookup?term=${encodeURIComponent(term)}`);
|
||||
if (resp.ok) {
|
||||
return await resp.json();
|
||||
}
|
||||
throw new Error(`Lookup failed: ${resp.status}`);
|
||||
},
|
||||
async create(payload) {
|
||||
return await fetch('/api/cards', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
},
|
||||
async update(id, payload) {
|
||||
return await fetch(`/api/cards/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
},
|
||||
async delete(id) {
|
||||
return await fetch(`/api/cards/${id}`, { method: 'DELETE' });
|
||||
},
|
||||
async batch(payload) {
|
||||
return await fetch('/api/cards/batch', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Collections & Tags
|
||||
collections: {
|
||||
async getAll() {
|
||||
const resp = await fetch('/api/collections');
|
||||
return resp.ok ? await resp.json() : [];
|
||||
}
|
||||
},
|
||||
tags: {
|
||||
async getAll() {
|
||||
const resp = await fetch('/api/tags');
|
||||
return resp.ok ? await resp.json() : [];
|
||||
}
|
||||
},
|
||||
|
||||
// Statistics & Analytics
|
||||
stats: {
|
||||
async getStats() {
|
||||
const resp = await fetch('/api/stats');
|
||||
return resp.ok ? await resp.json() : null;
|
||||
},
|
||||
async getSrsStats() {
|
||||
const resp = await fetch('/api/srs/stats');
|
||||
return resp.ok ? await resp.json() : null;
|
||||
},
|
||||
async getHeatmap(days = 180) {
|
||||
const resp = await fetch(`/api/srs/activity-heatmap?days=${days}`);
|
||||
return resp.ok ? await resp.json() : { days: [] };
|
||||
}
|
||||
},
|
||||
|
||||
// SRS Spaced Repetition
|
||||
srs: {
|
||||
async getQueue(forceAll = false, tagFilter = null) {
|
||||
let url = '/api/srs/queue?limit=50';
|
||||
if (forceAll) url += '&force_all=true';
|
||||
if (tagFilter) url += `&tag=${encodeURIComponent(tagFilter)}`;
|
||||
const resp = await fetch(url);
|
||||
return resp.ok ? await resp.json() : [];
|
||||
},
|
||||
async submitReview(cardId, rating) {
|
||||
return await fetch('/api/srs/review', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ card_id: cardId, rating })
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Bookshelf & Reader
|
||||
books: {
|
||||
async getAll() {
|
||||
const resp = await fetch('/api/books');
|
||||
return resp.ok ? await resp.json() : [];
|
||||
},
|
||||
async get(id) {
|
||||
const resp = await fetch(`/api/books/${id}`);
|
||||
if (resp.ok) return await resp.json();
|
||||
throw new Error(`Failed to load book ${id}: ${resp.status}`);
|
||||
},
|
||||
async uploadFile(formData) {
|
||||
return await fetch('/api/books', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
},
|
||||
async createManual(payload) {
|
||||
return await fetch('/api/books', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
},
|
||||
async delete(id) {
|
||||
return await fetch(`/api/books/${id}`, { method: 'DELETE' });
|
||||
},
|
||||
async saveProgress(bookId, payload) {
|
||||
return await fetch(`/api/books/${bookId}/progress`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user