initial commit

This commit is contained in:
2026-02-12 18:45:10 +01:00
commit be7bbba456
42 changed files with 3767 additions and 0 deletions

105
frontend/js/api.js Normal file
View File

@@ -0,0 +1,105 @@
/* FluentGerman.ai — API client with auth */
const API_BASE = '/api';
function getToken() {
return localStorage.getItem('fg_token');
}
function setToken(token) {
localStorage.setItem('fg_token', token);
}
function clearToken() {
localStorage.removeItem('fg_token');
}
function getUser() {
const data = localStorage.getItem('fg_user');
return data ? JSON.parse(data) : null;
}
function setUser(user) {
localStorage.setItem('fg_user', JSON.stringify(user));
}
function clearUser() {
localStorage.removeItem('fg_user');
}
async function api(path, options = {}) {
const token = getToken();
const headers = { ...options.headers };
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// Don't set Content-Type for FormData (browser sets it with boundary)
if (!(options.body instanceof FormData)) {
headers['Content-Type'] = headers['Content-Type'] || 'application/json';
}
const response = await fetch(`${API_BASE}${path}`, { ...options, headers });
if (response.status === 401) {
clearToken();
clearUser();
window.location.href = '/';
return;
}
return response;
}
async function apiJSON(path, options = {}) {
const response = await api(path, options);
if (!response || !response.ok) {
const error = await response?.json().catch(() => ({ detail: 'Request failed' }));
throw new Error(error.detail || 'Request failed');
}
return response.json();
}
function requireAuth() {
if (!getToken()) {
window.location.href = '/';
return false;
}
return true;
}
function requireAdmin() {
const user = getUser();
if (!user?.is_admin) {
window.location.href = '/chat.html';
return false;
}
return true;
}
function logout() {
clearToken();
clearUser();
window.location.href = '/';
}
/* Toast notifications */
function showToast(message, type = 'success') {
let container = document.querySelector('.toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'toast-container';
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 3000);
}