This commit is contained in:
2025-12-04 22:33:05 +01:00
parent 316175e158
commit 9dee06cdd6
145 changed files with 16865 additions and 88 deletions

109
public/assets/js/api.js Normal file
View File

@@ -0,0 +1,109 @@
// assets/js/api.js
const API = "api.php";
/** ---- intern: Hilfen ---- */
function withTs(url) {
const sep = url.includes("?") ? "&" : "?";
return `${url}${sep}t=${Date.now()}`; // no-store Absicherung
}
async function parseJsonSafe(res) {
const text = await res.text();
try {
return JSON.parse(text);
} catch (e) {
console.error("API: invalid JSON", { status: res.status, text });
return null;
}
}
// ... oberer Teil unverändert ...
/** zentraler Fetch-Wrapper: Credentials, no-store, 401→Login */
async function apiFetch(url, init = {}) {
const res = await fetch(withTs(url), {
credentials: "include",
cache: "no-store",
...init,
});
if (res.status === 401) {
window.location.href = "/login.php";
throw new Error("unauthorized");
}
return res;
}
/** ---- Public API ---- */
/**
* Action-Call:
* - apiAction('auth.me')
* - apiAction('sections.list', { method:'GET', data:{ template_id: 123 } })
* - apiAction('templates.create', { method:'POST', data:{ name:'...' } })
*/
export async function apiAction(
action,
{ method = "GET", data = null, headers = {} } = {}
) {
let url = `${API}?action=${encodeURIComponent(action)}`;
const init = { method, headers: { ...headers } };
// GET/HEAD → data als Query-String anhängen (kein Body!)
if ((method === "GET" || method === "HEAD") && data && typeof data === "object") {
const params = new URLSearchParams();
for (const [k, v] of Object.entries(data)) {
if (v !== undefined && v !== null) params.append(k, String(v));
}
const qs = params.toString();
if (qs) url += `&${qs}`;
} else if (data != null) {
init.headers["Content-Type"] = "application/json";
init.body = JSON.stringify(data);
}
const res = await apiFetch(url, init);
return await parseJsonSafe(res);
}
// ... Rest (apiList, apiCreate, apiUpdate, apiDelete, toast) unverändert ...
/**
* Listen-Helper für Ressourcen ruft `${res}.list` auf.
* Optional kannst du query-Objekte mitgeben, z.B. { template_id: 123 } für sections.
*/
export async function apiList(res, query = {}) {
const q = new URLSearchParams(query);
const qs = q.toString() ? `&${q.toString()}` : "";
const r = await apiAction(`${res}.list`, { method: "GET" });
// Falls du query serverseitig brauchst (z.B. template_id), nutze eine Action-Variante:
// return await apiAction(`${res}.list`, { method:"GET", data: query });
return r?.items ?? [];
}
/** GET by id: nur nutzen, wenn du eine `${res}.get`-Action hast */
export async function apiGet(res, id) {
return await apiAction(`${res}.get`, { method: "GET", data: { id } });
}
/** CREATE / UPDATE / DELETE sprechen `${res}.create|update|delete` an */
export async function apiCreate(res, payload) {
return await apiAction(`${res}.create`, { method: "POST", data: payload });
}
export async function apiUpdate(res, id, payload) {
return await apiAction(`${res}.update`, { method: "POST", data: { id, ...payload } });
}
export async function apiDelete(res, id) {
return await apiAction(`${res}.delete`, { method: "POST", data: { id } });
}
/** optionaler Toast-Fallback (keine harte Abhängigkeit) */
export function toast(msg, ok = true, opts = {}) {
if (window.Toast?.show) {
window.Toast.show(msg, { type: ok ? "success" : "error", duration: 2200, ...opts });
} else {
(ok ? console.log : console.error)(msg);
}
}