Python 64.6%
TypeScript 33.7%
CSS 0.8%
1/** Minimal API client: cookie session + bearer token (for SSE), French error messages. */23export const API_URL = import.meta.env.VITE_API_URL || '/api/v1';4const TOKEN_KEY = 'uqo.token';56export function getToken(): string | null {7 return localStorage.getItem(TOKEN_KEY);8}9export function setToken(token: string | null): void {10 if (token) localStorage.setItem(TOKEN_KEY, token);11 else localStorage.removeItem(TOKEN_KEY);12}1314export class ApiError extends Error {15 status: number;16 constructor(status: number, message: string) {17 super(message);18 this.status = status;19 }20}2122export function authHeaders(): Record<string, string> {23 const t = getToken();24 return t ? { Authorization: `Bearer ${t}` } : {};25}2627export async function api<T>(path: string, init: RequestInit = {}): Promise<T> {28 const headers: Record<string, string> = { ...authHeaders(), ...(init.headers as Record<string, string> | undefined) };29 if (init.body && !(init.body instanceof FormData)) headers['Content-Type'] = 'application/json';30 const res = await fetch(`${API_URL}${path}`, { ...init, headers, credentials: 'include' });31 if (res.status === 401) {32 setToken(null);33 window.dispatchEvent(new CustomEvent('uqo:unauthorized'));34 }35 if (!res.ok) {36 let detail = 'Une erreur est survenue.';37 try {38 const j = await res.json();39 detail = typeof j.detail === 'string' ? j.detail : JSON.stringify(j.detail);40 } catch {41 /* ignore */42 }43 throw new ApiError(res.status, detail);44 }45 if (res.status === 204) return undefined as T;46 return (await res.json()) as T;47}4849export const fileUrl = (fileId: string): string => `${API_URL}/files/${fileId}`;5051export async function downloadFile(fileId: string, filename: string): Promise<void> {52 const { url } = await api<{ url: string }>(`/files/${fileId}/link`);53 const a = document.createElement('a');54 a.href = url;55 a.download = filename;56 document.body.appendChild(a);57 a.click();58 a.remove();59}6061export async function shareFile(fileId: string, filename: string): Promise<boolean> {62 if (!navigator.share) return false;63 const res = await fetch(fileUrl(fileId), { headers: authHeaders(), credentials: 'include' });64 const blob = await res.blob();65 const file = new File([blob], filename, { type: blob.type });66 if (navigator.canShare && !navigator.canShare({ files: [file] })) return false;67 await navigator.share({ files: [file], title: filename });68 return true;69}70