Files
EventSnap/frontend/src/lib/api.ts
fabi f8cba95e49 chore(frontend): add ESLint + Prettier; fix real findings; format the tree
The frontend had no JS/TS linter — only svelte-check. Add flat-config ESLint (typescript-eslint
+ eslint-plugin-svelte) and Prettier (tabs/single-quote, matching the existing style).

Rules encode "catch bugs, not enforce taste":
  - svelte/require-each-key KEPT — it is the exact bug class as the feed mis-tap fix. Fixed every
    flagged block: keyed activeFilters, filteredUsers, stagedFiles (by previewUrl), captionTags,
    admin tabs/jobs/users, the export-viewer suggestions/filters/comments, and the static skeleton
    loops.
  - svelte/prefer-svelte-reactivity KEPT — inline-disabled only the verified-safe sites (a local
    freq Map in a $derived.by, throwaway URLSearchParams query builders), with a reason each.
  - svelte/no-navigation-without-resolve OFF — wants resolve() around every goto()/href; taste, not
    a bug, and pure churn.
  - svelte/no-unused-svelte-ignore OFF — those comments are consumed by svelte-check, which ESLint
    can't see, so it wrongly calls them unused; removing them would reintroduce a11y warnings.
  - no-explicit-any OFF for *.test.ts only (partial fixtures legitimately use any).

Real code fixes beyond keys: removed a dead jobLabel(), an unused ViewerComment import and unused
catch binding, an unused scroll-lock arg, and replaced an empty interface with a type alias.

Then `prettier --write` (54 files). Formatting only. Verified: eslint clean, svelte-check 0 errors,
vitest 46 passed, vite build succeeds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 20:45:42 +02:00

90 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { getToken, clearAuth } from './auth';
const BASE = '/api/v1';
export class ApiError extends Error {
status: number;
code: string;
constructor(status: number, code: string, message: string) {
super(message);
this.status = status;
this.code = code;
}
}
const TIMEOUT_MS = 20_000;
async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
const headers: Record<string, string> = {};
const token = getToken();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
}
// Abort hung requests so a dead connection surfaces as a friendly error
// instead of a spinner that never resolves.
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
let res: Response;
try {
res = await fetch(`${BASE}${path}`, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
signal: controller.signal
});
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') {
throw new ApiError(0, 'timeout', 'Zeitüberschreitung bitte erneut versuchen.');
}
throw new ApiError(0, 'network', 'Netzwerkfehler bitte Verbindung prüfen.');
} finally {
clearTimeout(timer);
}
if (res.status === 204) {
return undefined as T;
}
// A 5xx behind a proxy (or a crash page) can return HTML, not JSON — parsing
// it directly would throw an opaque SyntaxError. Read text, parse defensively.
const raw = await res.text();
let data: { error?: string; message?: string } | unknown = null;
if (raw) {
try {
data = JSON.parse(raw);
} catch {
data = null;
}
}
if (!res.ok) {
// An expired/invalid token (401) clears the dead session. Banned users are
// NOT logged out — they keep read access by design (USER_JOURNEYS §10) and
// simply get a 403 "gesperrt" toast on writes.
if (res.status === 401) {
clearAuth();
}
const d = (data ?? {}) as { error?: string; message?: string };
throw new ApiError(
res.status,
d.error ?? 'unknown',
d.message ?? `Serverfehler (${res.status}).`
);
}
return data as T;
}
export const api = {
get: <T>(path: string) => request<T>('GET', path),
post: <T>(path: string, body?: unknown) => request<T>('POST', path, body),
patch: <T>(path: string, body?: unknown) => request<T>('PATCH', path, body),
delete: <T>(path: string) => request<T>('DELETE', path)
};